feat: core bookkeeping UI + property management#46
Conversation
Add four new finance pages consuming existing backend APIs: - Transactions: full table with filters, search, sort, add dialog, CSV export, reconcile toggle, pagination (50/page) - Accounts: cards grouped by type, balance summary (assets/liabilities/ net worth), detail drill-down with account transactions, add dialog - Reports: consolidated P&L, by-entity and by-state breakdowns, quality metrics, tax readiness checklist, tax automation trigger - Integrations: sync status cards, Wave sync, TurboTenant CSV import, recurring charges with optimization recommendations Backend additions: - POST /api/transactions (create) - PATCH /api/transactions/:id (update/reconcile) - POST /api/accounts (general account creation) - SystemStorage.updateTransaction() method Property management (prior session): - AddUnit, AddLease, EditProperty dialogs - CommsPanel (SMS/email via Twilio/SendGrid) - WorkflowBoard (maintenance/expense approvals) - WorkspaceTab with Google Workspace embeds - Google OAuth, comms, and workflow routes - comms_log and workflows schema tables Navigation: 4 new sidebar items with role-based visibility (Transactions/Accounts = bookkeeper+, Reports/Integrations = accountant+) Verified: tsc --noEmit clean, 135/135 tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
chittyfinance | 9289839 | Mar 12 2026, 11:03 AM |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Google Workspace, Twilio, and SendGrid integrations; new client pages (Transactions, Accounts, Reports, Integrations); expanded property features (units, leases, workspace embeds, comms, workflows); new client hooks; new server routes/libs for Google/comms/workflows; DB schema additions for comms logs and workflows. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Client as Client (Browser)
participant Server as App Server
participant Google as Google OAuth/API
participant Storage as DB/Storage
User->>Client: Click "Connect Google Workspace"
Client->>Server: GET /api/integrations/google/authorize
Server->>Google: Request auth URL (client id, scopes, redirect)
Google-->>Server: auth URL
Server-->>Client: { authUrl }
Client->>Google: Browser -> Google OAuth (user consents)
Google-->>Client: Redirect to /api/integrations/google/callback?code=...
Client->>Server: GET /api/integrations/google/callback?code=...
Server->>Google: Exchange code for tokens
Google-->>Server: access_token/refresh_token
Server->>Storage: persist integration credentials
Storage-->>Server: stored
Server-->>Client: Redirect to app (connected)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 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 |
PR Review: feat: core bookkeeping UI + property managementThis is a substantial PR (+3960/-20 lines) adding Transactions, Accounts, Reports, and Integrations pages plus Google Workspace, Twilio, SendGrid integrations and approval workflows. Overall the architecture is consistent with the codebase and the component patterns are clean. Below are findings ranging from blocking security issues to minor improvements. 🔴 Blocking: Security Issues1. Workflow tenant isolation is broken
// system.ts — getWorkflow has no tenant filter
async getWorkflow(id: string) {
const [row] = await this.db.select().from(schema.workflows).where(eq(schema.workflows.id, id));
return row;
}The route at const workflow = await storage.updateWorkflow(id, { status: body.status, ... });
// tenantId is available via c.get('tenantId') but is never passedCompare with .where(and(eq(schema.transactions.id, id), eq(schema.transactions.tenantId, tenantId)))
2. SMS/email
commsRoutes.post('/api/comms/send', async (c) => {
const { channel, to, message } = body;
// No format validation on `to`
const result = await twilio.sendSms(to, message);At minimum, validate E.164 format for phone numbers and RFC 5321 for emails. Consider rate-limiting per tenant. 3. Google Drive query is constructed via string interpolation
const q = `'${folderId}' in parents and trashed = false`;If 4. Google OAuth: expired tokens are used without refresh
const creds = integration.credentials as Record<string, string>;
const result = await createCalendarEvent(creds.access_token, ...);Stale tokens will fail with a 🟡 Bugs5. React Hooks rule violation in
if (!tenantId) {
return <div>Select a tenant...</div>; // ← early return
}
// ❌ Hook called conditionally — will crash at runtime
const grouped = useMemo(() => { ... }, [accounts]);Move the 6. Missing required field validation in
const workflow = await storage.createWorkflow({
tenantId,
title: body.title, // ← could be undefinedThe schema column is if (!body.title) return c.json({ error: 'title is required' }, 400);7.
Clear onValueChange={v => { setChannel(v as 'sms' | 'email'); setRecipient(''); }}8. Dead code in message: channel === 'email' ? message : message,Both branches are identical. The ternary can be removed. 9.
10. Unused Both components declare export default function GoogleDriveEmbed({ folderId, connected }: GoogleDriveEmbedProps) {
// ^^^^^^^^ propertyId is silently droppedEither use it or remove it from the interface. 11.
🔵 Code Quality12. function formatTransaction(t: any) {Use 13. onSuccess: (data: any) => toast({ title: `Wave sync: ${data.imported ?? 0} transactions imported` }),Type the mutation return or assert the shape inline. 14. Default fallback OAuth secret should warn louder
const secret = c.env.OAUTH_STATE_SECRET || 'default-secret-change-in-production';Same pattern exists in the Wave route. Consider logging a 📋 Performance Note15. All transactions fetched client-side
✅ What's done well
Summary of blocking items
🤖 Reviewed with Claude Code |
There was a problem hiding this comment.
Pull request overview
Adds a core bookkeeping UI (transactions/accounts/reports/integrations) and expands property management with communications, workflows, and Google Workspace embeds, backed by new API routes and storage methods.
Changes:
- Implement Transactions/Accounts/Reports/Integrations pages with new React Query hooks and sidebar/nav wiring.
- Add backend endpoints for creating/updating transactions, creating accounts, and new comms/workflow/google integration routes.
- Extend system storage and DB schema to support comms logs and workflow records, plus Google/Twilio/SendGrid edge clients.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| server/storage/system.ts | Adds updateTransaction, comms log storage, and workflow CRUD helpers |
| server/routes/workflows.ts | New workflows API (list/create/update/approve/complete) |
| server/routes/transactions.ts | Adds POST + PATCH transaction endpoints; refactors response formatting |
| server/routes/google.ts | New Google OAuth + Calendar/Drive API proxy routes |
| server/routes/comms.ts | New comms send/template/history/status routes using Twilio/SendGrid |
| server/routes/accounts.ts | Adds generic POST /api/accounts endpoint |
| server/lib/twilio.ts | Edge-compatible Twilio SMS client + SMS templates |
| server/lib/sendgrid.ts | Edge-compatible SendGrid client + email templates |
| server/lib/google-api.ts | Edge-compatible Google OAuth + Calendar/Drive REST helpers + embed URL helpers |
| server/env.ts | Adds env vars for Google/Twilio/SendGrid configuration |
| server/app.ts | Mounts new route groups and protects new API prefixes |
| database/system.schema.ts | Adds comms_log and workflows tables + Zod schemas/types |
| client/src/pages/Transactions.tsx | New transactions table UI with filters/sort/pagination/create/reconcile + CSV export |
| client/src/pages/Accounts.tsx | New accounts overview + detail drill-down + create/sync actions |
| client/src/pages/Reports.tsx | New consolidated P&L + data quality metrics + tax automation trigger |
| client/src/pages/Integrations.tsx | New integrations status, Wave sync, TurboTenant import, recurring charges, optimizations UI |
| client/src/pages/PropertyDetail.tsx | Adds property dialogs + workspace/comms/workflows tabs |
| client/src/pages/Connections.tsx | Adds Google Workspace integration card + OAuth connect handler |
| client/src/components/layout/Sidebar.tsx | Adds sidebar nav items for Transactions/Accounts/Reports/Integrations with role gating |
| client/src/App.tsx | Registers new routes for the new pages |
| client/src/hooks/use-transactions.ts | New hooks for listing/creating/updating transactions |
| client/src/hooks/use-accounts.ts | New hooks for accounts list/create/sync + account transactions |
| client/src/hooks/use-reports.ts | New consolidated report hook + tax automation mutation |
| client/src/hooks/use-integrations.ts | New hooks for integrations, charges, optimizations, Wave sync, TurboTenant import |
| client/src/components/property/WorkspaceTab.tsx | Property workspace tab using Google integration status + embeds |
| client/src/components/property/GoogleCalendarEmbed.tsx | Calendar iframe + “add event” dialog hitting /api/google/calendar/events |
| client/src/components/property/GoogleSheetsEmbed.tsx | Sheets iframe embed panel |
| client/src/components/property/GoogleDriveEmbed.tsx | Drive folder iframe embed panel |
| client/src/components/property/CommsPanel.tsx | Property comms UI (send + history) |
| client/src/components/property/WorkflowBoard.tsx | Property workflow kanban UI + create/update actions |
| client/src/components/property/AddUnitDialog.tsx | Add-unit dialog |
| client/src/components/property/AddLeaseDialog.tsx | Add-lease dialog |
| client/src/components/property/EditPropertyDialog.tsx | Edit-property dialog |
| client/src/components/property/PropertyDetailPanel.tsx | Adds unit/lease/edit dialogs to the side panel |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (12)
client/src/pages/Connections.tsx-124-135 (1)
124-135:⚠️ Potential issue | 🟡 MinorClear the Google pending state when no auth URL comes back.
If Line 127 returns non-OK or the payload omits
authUrl, Lines 129-130 fall through without resettingconnectingType. The Google card then stays stuck on “Connecting...” until refresh.Suggested fix
const connectGoogle = async () => { setConnectingType('google'); try { const response = await fetch('/api/integrations/google/authorize'); + if (!response.ok) { + throw new Error('Failed to start Google authorization'); + } const data: { authUrl?: string } = await response.json(); - if (data.authUrl) { - window.location.href = data.authUrl; - } + if (!data.authUrl) { + throw new Error('Missing Google auth URL'); + } + window.location.href = data.authUrl; } catch (error) { console.error('Failed to start Google authorization:', error); setConnectingType(null); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/pages/Connections.tsx` around lines 124 - 135, The connectGoogle function can leave connectingType set when the fetch returns non-OK or the response lacks authUrl; update connectGoogle to check response.ok and whether data.authUrl exists and call setConnectingType(null) before returning when either case fails (and also in any early-return path), so the Google card doesn't stay stuck; reference the connectGoogle function and the setConnectingType state updater to implement these checks and ensure connectingType is cleared on all failure or missing-authUrl paths.client/src/components/property/WorkflowBoard.tsx-57-59 (1)
57-59:⚠️ Potential issue | 🟡 MinorDon’t show the empty-state copy while workflows are still loading or errored.
Because
datadefaults to[], each column rendersNoneuntil the request resolves and also after fetch failures. That makes populated properties look empty and hides the real failure mode.As per coding guidelines,
client/src/**/*.{ts,tsx}: Use TanStack React Query for all API data fetching; handle loading and error states with appropriate UI feedback.Also applies to: 97-107
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/WorkflowBoard.tsx` around lines 57 - 59, The component currently defaults `data` to `[]` in the useQuery call which makes columns render "None" during loading or on error; instead remove the `= []` default and use the returned flags from useQuery (isLoading, isError, error) alongside `data` (e.g., the `workflows` variable from useQuery) to show a loading indicator while isLoading is true and an error UI when isError is true, only falling back to an empty array for rendering columns after a successful fetch (or use `data ?? []` after checking status). Update the render logic around where columns are built (the code around lines with `workflows` and the columns at 97-107) to check these flags before displaying the empty-state copy.client/src/components/property/PropertyDetailPanel.tsx-104-106 (1)
104-106:⚠️ Potential issue | 🟡 MinorGive the edit icon button an accessible name.
Line 104 renders only a
Pencilicon, so assistive tech will announce a generic button. Addaria-label="Edit property"or visible text.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/PropertyDetailPanel.tsx` around lines 104 - 106, The edit icon button in PropertyDetailPanel currently renders only <Button ... onClick={() => setEditPropertyOpen(true)}> with a <Pencil /> child, which is not accessible; update that Button (the one that calls setEditPropertyOpen in PropertyDetailPanel) to include an accessible name — e.g., add aria-label="Edit property" or include visually hidden text alongside the <Pencil /> icon so screen readers announce "Edit property". Ensure the attribute is placed on the Button component (not the icon) so assistive tech receives the label.client/src/pages/Integrations.tsx-38-43 (1)
38-43:⚠️ Potential issue | 🟡 MinorAdd loading/error UI for the other query-backed sections.
Only
useIntegrations()contributesisLoading.charges,optimizations, andaccountsdefault to[], so this page renders empty-state copy or an empty picker while those requests are still pending or failed.As per coding guidelines, "Use TanStack React Query for all API data fetching; handle loading and error states with appropriate UI feedback."
Also applies to: 152-179, 183-215, 223-229
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/pages/Integrations.tsx` around lines 38 - 43, The page is only handling loading for useIntegrations(); update the other query hooks—useRecurringCharges, useChargeOptimizations, useAccounts, useWaveSync, and useTurboTenantImport—to destructure and use their isLoading/isFetching and isError (plus error) flags instead of silently defaulting to [] so the UI can show proper loading spinners and error states; specifically, change the const lines that set charges, optimizations, and accounts to also extract isLoading and isError (e.g., const { data: charges, isLoading: chargesLoading, isError: chargesError, error: chargesErrorObj } = useRecurringCharges()), then in the matching sections for charges, optimizations, accounts, waveSync and ttImport render a spinner/placeholder when loading and an error banner/message when isError using the error object, keeping empty-state copy only when data is loaded and empty.client/src/hooks/use-integrations.ts-60-69 (1)
60-69:⚠️ Potential issue | 🟡 MinorRefresh the charge-analysis caches after imports and syncs.
Both mutations change transaction data, but neither invalidates
['/api/charges/recurring', tenantId]or['/api/charges/optimizations', tenantId]. The Integrations page will keep showing pre-sync recommendations until a manual reload.Suggested fix
export function useWaveSync() { const qc = useQueryClient(); const tenantId = useTenantId(); return useMutation({ mutationFn: () => apiRequest('POST', '/api/import/wave-sync', {}).then(r => r.json()), onSuccess: () => { qc.invalidateQueries({ queryKey: ['/api/transactions', tenantId] }); qc.invalidateQueries({ queryKey: ['/api/integrations', tenantId] }); + qc.invalidateQueries({ queryKey: ['/api/charges/recurring', tenantId] }); + qc.invalidateQueries({ queryKey: ['/api/charges/optimizations', tenantId] }); }, }); } @@ export function useTurboTenantImport() { const qc = useQueryClient(); const tenantId = useTenantId(); return useMutation({ @@ onSuccess: () => { qc.invalidateQueries({ queryKey: ['/api/transactions', tenantId] }); + qc.invalidateQueries({ queryKey: ['/api/charges/recurring', tenantId] }); + qc.invalidateQueries({ queryKey: ['/api/charges/optimizations', tenantId] }); }, }); }Also applies to: 73-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/hooks/use-integrations.ts` around lines 60 - 69, The onSuccess handler in useWaveSync (and the other integration mutation block around lines 73-92) fails to invalidate charge-analysis caches, so add qc.invalidateQueries calls for the two charge query keys; specifically, in useWaveSync's onSuccess (where qc and tenantId are available) call qc.invalidateQueries({ queryKey: ['/api/charges/recurring', tenantId] }) and qc.invalidateQueries({ queryKey: ['/api/charges/optimizations', tenantId] }), and make the same additions to the onSuccess of the other integration mutation mentioned (so both mutations refresh charge-analysis data after imports/syncs).client/src/hooks/use-accounts.ts-67-75 (1)
67-75:⚠️ Potential issue | 🟡 MinorInvalidate the account-transactions query after sync.
useSyncAccount()only refreshes the account list. If the sync imports new ledger entries, the drill-down backed byuseAccountTransactions()keeps showing stale data until a reload.Suggested fix
export function useSyncAccount() { const qc = useQueryClient(); const tenantId = useTenantId(); return useMutation({ mutationFn: ({ id, ...data }: { id: string } & Record<string, unknown>) => apiRequest('POST', `/api/accounts/${id}/sync`, data).then(r => r.json()), - onSuccess: () => { + onSuccess: (_data, { id }) => { qc.invalidateQueries({ queryKey: ['/api/accounts', tenantId] }); + qc.invalidateQueries({ queryKey: [`/api/accounts/${id}/transactions`, tenantId] }); }, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/hooks/use-accounts.ts` around lines 67 - 75, useSyncAccount currently only invalidates the account list and misses refreshing per-account transactions; update the mutation's onSuccess to accept the mutation variables (e.g. onSuccess: (_data, variables) => { ... }) and after invalidating ['/api/accounts', tenantId] also call qc.invalidateQueries for the account transactions query using the synced account id, e.g. invalidateQueries({ queryKey: ['/api/account-transactions', tenantId, variables.id] }) so useAccountTransactions-backed UI refreshes after a sync.server/routes/google.ts-155-167 (1)
155-167:⚠️ Potential issue | 🟡 MinorMissing Zod validation for folder creation endpoint.
Same issue as the calendar events endpoint - the request body should be validated with Zod.
🛡️ Proposed fix: Add Zod validation
+const createFolderSchema = z.object({ + name: z.string().min(1), + parentId: z.string().optional(), +}); // POST /api/google/drive/folders — Create Drive folder for property googleRoutes.post('/api/google/drive/folders', async (c) => { const storage = c.get('storage'); const tenantId = c.get('tenantId'); const integration = (await storage.getIntegrations(tenantId)).find(i => i.serviceType === 'google'); if (!integration?.connected) return c.json({ error: 'Google not connected' }, 400); const creds = integration.credentials as Record<string, string>; - const body = await c.req.json(); + const parseResult = createFolderSchema.safeParse(await c.req.json()); + if (!parseResult.success) { + return c.json({ error: 'Invalid request body', details: parseResult.error.flatten() }, 400); + } + const body = parseResult.data;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/routes/google.ts` around lines 155 - 167, The POST /api/google/drive/folders route lacks request-body validation; add a Zod schema (e.g., FolderCreateSchema) and validate the parsed body before calling createDriveFolder so body.name and optional body.parentId are type-safe. If validation fails return a 400 with the validation errors; on success use the parsed value when calling createDriveFolder(creds.access_token, parsed.name, parsed.parentId). Update the route handler where googleRoutes.post('/api/google/drive/folders', ...) is defined and ensure the validated type replaces the raw body variable.client/src/pages/Reports.tsx-265-270 (1)
265-270:⚠️ Potential issue | 🟡 MinorPotential runtime issue if
c.statushas unexpected values.The
STATUS_ICONobject only has keys for'pass','warn', and'fail'. Ifc.statuscontains any other value,STATUS_ICON[c.status]would beundefined, rendering nothing silently.💡 Add fallback for unexpected status values
{checks.map(c => ( <div key={c.id} className="flex items-center gap-2 py-1"> - {STATUS_ICON[c.status]} + {STATUS_ICON[c.status as keyof typeof STATUS_ICON] ?? <AlertTriangle className="w-3.5 h-3.5 text-gray-400" />} <span className="text-xs text-[hsl(var(--cf-text-secondary))]">{c.message}</span> </div> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/pages/Reports.tsx` around lines 265 - 270, STATUS_ICON lookup can return undefined when c.status is not 'pass'|'warn'|'fail'; update the rendering inside checks.map (the block using checks.map, c.status, and STATUS_ICON) to provide a safe fallback icon/component when STATUS_ICON[c.status] is undefined (e.g., use a default icon entry in STATUS_ICON like 'unknown' or fall back to STATUS_ICON['warn'] / a placeholder JSX element) so the UI never renders nothing for unexpected status values.server/routes/google.ts-105-123 (1)
105-123:⚠️ Potential issue | 🟡 MinorMissing Zod validation for POST request body.
The request body is used directly without Zod schema validation. This violates the coding guidelines and could lead to unexpected errors or security issues if malformed data is passed.
🛡️ Proposed fix: Add Zod validation
+import { z } from 'zod'; + +const createEventSchema = z.object({ + calendarId: z.string().optional(), + summary: z.string().min(1), + description: z.string().optional(), + startDateTime: z.string(), + endDateTime: z.string(), +}); // POST /api/google/calendar/events — Create calendar event googleRoutes.post('/api/google/calendar/events', async (c) => { const storage = c.get('storage'); const tenantId = c.get('tenantId'); const integration = (await storage.getIntegrations(tenantId)).find(i => i.serviceType === 'google'); if (!integration?.connected) return c.json({ error: 'Google not connected' }, 400); const creds = integration.credentials as Record<string, string>; - const body = await c.req.json(); + const parseResult = createEventSchema.safeParse(await c.req.json()); + if (!parseResult.success) { + return c.json({ error: 'Invalid request body', details: parseResult.error.flatten() }, 400); + } + const body = parseResult.data; const calendarId = body.calendarId || 'primary';As per coding guidelines: "Use Zod schemas for input validation on all API endpoints."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/routes/google.ts` around lines 105 - 123, The POST handler googleRoutes.post('/api/google/calendar/events', ...) uses the request body without validation; add a Zod schema (e.g., CreateCalendarEventSchema using z.object) that validates calendarId as optional string, summary as nonempty string, description as optional string, and startDateTime/endDateTime as ISO datetime strings (z.string().refine or z.preprocess with Date if you prefer), import z from 'zod', parse the incoming body with schema.safeParse/parse and return a 400 JSON error on validation failure, then use the validated payload to call createCalendarEvent so the handler only operates on typed/validated input.client/src/components/property/CommsPanel.tsx-80-91 (1)
80-91:⚠️ Potential issue | 🟡 MinorRedundant ternary and potential UX issue with channel switching.
- Line 85:
channel === 'email' ? message : messagealways returnsmessage- this is redundant.- When a user selects a tenant, then switches channels, the recipient contact (phone vs email) doesn't update to match the new channel.
🧹 Proposed fixes
function handleSend() { if (!recipient || !message) return; sendMutation.mutate({ channel, to: recipient, - message: channel === 'email' ? message : message, + message, subject: channel === 'email' ? (subject || 'ChittyFinance Notification') : undefined, recipientName, propertyId, template: template !== 'custom' ? template : undefined, }); }For the channel switching issue, consider storing the selected lease ID and updating recipient when channel changes:
+ const [selectedLeaseId, setSelectedLeaseId] = useState<string | null>(null); + + // Update recipient when channel changes if a lease is selected + useEffect(() => { + if (selectedLeaseId) { + selectTenant(selectedLeaseId); + } + }, [channel]); function selectTenant(leaseId: string) { const lease = leases.find(l => l.id === leaseId); if (!lease) return; + setSelectedLeaseId(leaseId); setRecipientName(lease.tenantName);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/CommsPanel.tsx` around lines 80 - 91, The send handler contains a redundant ternary and doesn't update recipient when channel changes; in handleSend replace the unnecessary channel === 'email' ? message : message with just message and ensure subject is only passed for email (keep subject logic), and implement logic to track selected lease/contact (e.g., selectedLeaseId or selectedContact) and update the recipient field when channel changes (listen for channel changes in the component and switch recipient to the lease's email or phone accordingly) so the recipient always matches the current channel before calling sendMutation.mutate.client/src/pages/Reports.tsx-65-89 (1)
65-89:⚠️ Potential issue | 🟡 MinorCSV export has potential issues with special characters and missing data sanitization.
The CSV export doesn't escape quotes or commas in entity names, which could corrupt the CSV if names contain these characters. Also, if
reportbecomes undefined between the check and usage (unlikely but possible with async state), it could cause issues.🛡️ Proposed fix for CSV escaping
const exportCsv = () => { if (!report) return; + const escapeCell = (val: string | number) => { + const str = String(val); + return str.includes(',') || str.includes('"') ? `"${str.replace(/"/g, '""')}"` : str; + }; const rows = [ 'Consolidated P&L Report', `Period: ${report.scope.startDate} to ${report.scope.endDate}`, '', 'TOTALS', `Income,${report.totals.income}`, `Expenses,${report.totals.expenses}`, `Deductible Expenses,${report.totals.deductibleExpenses}`, `Net Income,${report.totals.net}`, '', 'BY ENTITY', 'Entity,Type,Income,Expenses,Net', - ...report.byEntity.map(e => `"${e.tenantName}",${e.tenantType},${e.income},${e.expenses},${e.net}`), + ...report.byEntity.map(e => `${escapeCell(e.tenantName)},${escapeCell(e.tenantType)},${e.income},${e.expenses},${e.net}`), '', 'BY STATE', 'State,Income,Expenses,Taxable Income,Estimated Tax', - ...report.byState.map(s => `${s.state},${s.income},${s.expenses},${s.taxableIncome},${s.estimatedTax}`), + ...report.byState.map(s => `${escapeCell(s.state)},${s.income},${s.expenses},${s.taxableIncome},${s.estimatedTax}`), ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/pages/Reports.tsx` around lines 65 - 89, The exportCsv function should defensively snapshot report at the start and properly escape/sanitize all CSV fields: capture const rpt = report at top of exportCsv to avoid it becoming undefined mid-run, then when building rows ensure every field from rpt.totals, rpt.byEntity and rpt.byState is normalized (use default empty string or 0) and string fields are CSV-escaped by wrapping them in double quotes and doubling any internal double quotes (e.g., tenantName -> "\"" + tenantName.replace(/"/g, '""') + "\""); apply this escaping to the Entity row fields and any other string columns (state, dates), and use the sanitized startDate/endDate (from rpt.scope) when constructing the download filename before creating the Blob/URL and triggering the download.server/routes/google.ts-108-122 (1)
108-122:⚠️ Potential issue | 🟡 MinorAdd automatic token refresh before Google API calls to prevent 401 failures when tokens expire.
The code stores
expires_atin credentials but never checks it before usingcreds.access_token. When the token expires, all Google API calls (calendar, drive) will fail with 401 errors. Implement token refresh logic similar to the Wave integration: check ifexpires_athas passed, and if so, callGoogleAPIClient.refreshToken(creds.refresh_token)to obtain a new token before making API requests. This should be done in all routes that use the access token (lines 108–123, 126–139, 142–153, 156–167).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/routes/google.ts` around lines 108 - 122, Check credentials' expires_at before using creds.access_token in the Google routes (where you call storage.getIntegrations and use integration.credentials and createCalendarEvent); if expires_at is past, call GoogleAPIClient.refreshToken(creds.refresh_token) to get a new access_token (and updated expires_at), persist the updated credentials back to storage for the tenant, and then use the refreshed access_token for the API call; apply the same pattern to all routes that call Google APIs (calendar, drive) to avoid 401s.
🧹 Nitpick comments (9)
server/lib/twilio.ts (1)
26-47: Add timeout to prevent indefinite hanging on network issues.The
fetchcall has no timeout configured. If Twilio is slow or unresponsive, this could block the request thread indefinitely.♻️ Proposed fix - add AbortController timeout
async sendSms(to: string, body: string): Promise<SendSmsResult> { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout + - const res = await fetch(`${this.baseUrl}/Messages.json`, { - method: 'POST', - headers: { - Authorization: this.authHeader, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - To: to, - From: this.config.fromNumber, - Body: body, - }), - }); + try { + const res = await fetch(`${this.baseUrl}/Messages.json`, { + method: 'POST', + headers: { + Authorization: this.authHeader, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + To: to, + From: this.config.fromNumber, + Body: body, + }), + signal: controller.signal, + }); + + clearTimeout(timeoutId); if (!res.ok) { const err = await res.text(); throw new Error(`Twilio send failed (${res.status}): ${err}`); } const data: Record<string, string> = await res.json(); return { sid: data.sid, status: data.status, to: data.to }; + } finally { + clearTimeout(timeoutId); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/lib/twilio.ts` around lines 26 - 47, sendSms currently calls fetch without a timeout which can hang; add an AbortController-based timeout inside sendSms to abort the fetch after a configurable interval (e.g., 10s), pass controller.signal to fetch, and clear the timer on success. Specifically, inside the sendSms method create an AbortController, start a setTimeout that calls controller.abort(), include signal: controller.signal in the fetch options, and clearTimeout when the response is received; also handle the abort case by catching the thrown error and rethrowing a clear error (e.g., "Twilio send timed out") so callers of sendSms can distinguish timeouts from other failures.server/routes/transactions.ts (1)
6-20: Avoidanytype - use proper typing for transaction parameter.The
formatTransactionfunction loses type safety by acceptingany. Consider using the inferred schema type.♻️ Proposed fix
+import type { Transaction } from '@shared/schema'; + -function formatTransaction(t: any) { +function formatTransaction(t: Transaction) { return { id: t.id, accountId: t.accountId, amount: parseFloat(t.amount), type: t.type, category: t.category || undefined, description: t.description, date: t.date, payee: t.payee || undefined, propertyId: t.propertyId || undefined, unitId: t.unitId || undefined, reconciled: t.reconciled, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/routes/transactions.ts` around lines 6 - 20, The function formatTransaction currently accepts an untyped parameter (any) which loses type safety; update formatTransaction to accept the correct transaction input type from your schema/model (e.g., the inferred type exported by your validation schema or DB model — e.g., TransactionInput, TransactionDTO, or use ReturnType<typeof transactionSchema.parse>) so the compiler can validate properties like amount, id, accountId, etc.; import or derive that type and replace the parameter type, and adjust any conversions (e.g., parseFloat(t.amount)) only if the schema/type allows string amounts to ensure type correctness and avoid using any.client/src/components/property/GoogleDriveEmbed.tsx (1)
4-10: UnusedpropertyIdprop - same issue asGoogleSheetsEmbed.The
propertyIdis declared in the interface but not used. Consider removing it or documenting its intended future use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/GoogleDriveEmbed.tsx` around lines 4 - 10, GoogleDriveEmbedProps declares a propertyId that is never used in the component; either remove propertyId from the GoogleDriveEmbedProps interface and from the component signature or, if it’s intended for future use, add a clear TODO comment and use it (for example pass it to GoogleDriveEmbed or downstream logic). Update the interface GoogleDriveEmbedProps and the component function GoogleDriveEmbed accordingly, and mirror the same fix you applied for GoogleSheetsEmbed so prop types and usage remain consistent.client/src/components/property/WorkspaceTab.tsx (1)
18-24: Consider adding loading/error state handling for better UX.The
useQueryhook doesn't handle loading or error states. While the child components may gracefully handleundefinedvalues, showing a loading indicator or error message would improve the user experience.💡 Optional: Add loading/error handling
export default function WorkspaceTab({ propertyId }: WorkspaceTabProps) { - const { data: status } = useQuery<GoogleStatus>({ + const { data: status, isLoading, error } = useQuery<GoogleStatus>({ queryKey: ['/api/integrations/google/status'], staleTime: 5 * 60 * 1000, }); const connected = status?.connected ?? false; + if (isLoading) { + return <div className="p-4 text-muted-foreground">Loading workspace...</div>; + } + return (As per coding guidelines: "Use TanStack React Query for all API data fetching; handle loading and error states with appropriate UI feedback."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/WorkspaceTab.tsx` around lines 18 - 24, The WorkspaceTab component currently reads only data from useQuery and derives connected from status; update the useQuery call to destructure isLoading, isError, and error (e.g., const { data: status, isLoading, isError, error } = useQuery(...)) and add early-return UI: render a loading indicator when isLoading is true and render a concise error message (using error.message) when isError is true before deriving connected or rendering child components — keep this logic inside WorkspaceTab so downstream components always receive defined props.client/src/components/property/GoogleSheetsEmbed.tsx (1)
4-10: UnusedpropertyIdprop.The
propertyIdis declared in the interface but not used in the component. Either remove it from the interface or use it (e.g., for a future feature like creating property-specific spreadsheets).🧹 Remove unused prop
interface GoogleSheetsEmbedProps { - propertyId: string; sheetId?: string; connected: boolean; } -export default function GoogleSheetsEmbed({ sheetId, connected }: GoogleSheetsEmbedProps) { +export default function GoogleSheetsEmbed({ sheetId, connected }: GoogleSheetsEmbedProps) {Note: If removing
propertyId, also update the parentWorkspaceTab.tsxto stop passing it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/GoogleSheetsEmbed.tsx` around lines 4 - 10, The interface GoogleSheetsEmbedProps declares propertyId but the component GoogleSheetsEmbed does not use it; either remove propertyId from GoogleSheetsEmbedProps and from the GoogleSheetsEmbed parameter list, and update the caller in WorkspaceTab.tsx to stop passing propertyId, or actually use propertyId inside GoogleSheetsEmbed (e.g., accept it in the signature and reference it where needed for future feature hooks/URLs); locate the symbols GoogleSheetsEmbedProps and function GoogleSheetsEmbed to make the change and also update the parent component WorkspaceTab.tsx to match the new prop shape.client/src/components/property/GoogleCalendarEmbed.tsx (2)
113-129: AddhtmlForattributes to Labels for accessibility.Labels lack
htmlForattributes and inputs lack correspondingidattributes. This breaks the explicit label-input association that screen readers depend on.♿ Proposed fix for accessibility
<div className="space-y-2"> - <Label>Title *</Label> - <Input value={summary} onChange={e => setSummary(e.target.value)} required placeholder="e.g. Maintenance inspection" /> + <Label htmlFor="event-title">Title *</Label> + <Input id="event-title" value={summary} onChange={e => setSummary(e.target.value)} required placeholder="e.g. Maintenance inspection" /> </div> <div className="space-y-2"> - <Label>Description</Label> - <Input value={description} onChange={e => setDescription(e.target.value)} placeholder="Optional details" /> + <Label htmlFor="event-description">Description</Label> + <Input id="event-description" value={description} onChange={e => setDescription(e.target.value)} placeholder="Optional details" /> </div> <div className="grid grid-cols-2 gap-3"> <div className="space-y-2"> - <Label>Start *</Label> - <Input type="datetime-local" value={startDateTime} onChange={e => setStartDateTime(e.target.value)} required /> + <Label htmlFor="event-start">Start *</Label> + <Input id="event-start" type="datetime-local" value={startDateTime} onChange={e => setStartDateTime(e.target.value)} required /> </div> <div className="space-y-2"> - <Label>End *</Label> - <Input type="datetime-local" value={endDateTime} onChange={e => setEndDateTime(e.target.value)} required /> + <Label htmlFor="event-end">End *</Label> + <Input id="event-end" type="datetime-local" value={endDateTime} onChange={e => setEndDateTime(e.target.value)} required /> </div> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/GoogleCalendarEmbed.tsx` around lines 113 - 129, The Labels in GoogleCalendarEmbed.tsx are missing htmlFor attributes and the corresponding Inputs lack id attributes, breaking screen-reader associations; fix by adding unique id props to each Input (e.g. summary-input, description-input, start-datetime-input, end-datetime-input) and set the matching htmlFor on the corresponding Label components that wrap the summary, description, startDateTime and endDateTime fields (ensure the id strings match exactly and remain stable if variables change).
67-76: UnusedpropertyIdparameter in type signature.
propertyIdis declared as a required property in the type (line 72) but is not destructured from props (line 68) and never used in the function body. Either remove it from the type signature or destructure and use it if needed for future functionality.🧹 Proposed fix to remove unused parameter
function AddEventDialog({ calendarId, open, onOpenChange, }: { - propertyId: string; calendarId?: string; open: boolean; onOpenChange: (open: boolean) => void; }) {Also update the call site if you remove the prop:
<AddEventDialog - propertyId={propertyId} calendarId={calendarId} open={addEventOpen} onOpenChange={setAddEventOpen} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/src/components/property/GoogleCalendarEmbed.tsx` around lines 67 - 76, The AddEventDialog component's props type includes a required propertyId that is not destructured or used; either remove propertyId from the props type or destructure and use it where needed. To fix: update the AddEventDialog signature (the props type object around AddEventDialog) by removing propertyId from the type definition if it's not needed and adjust any call sites that pass propertyId, or modify the function to accept ({ propertyId, calendarId, open, onOpenChange }) and use propertyId in the component logic; ensure the props type and all callers of AddEventDialog (where it's instantiated) are kept consistent.server/lib/google-api.ts (2)
98-99: Inconsistent error handling across helper functions.The class methods (
exchangeCode,refreshToken) include the response body in errors, but the helper functions only include the status code. This makes debugging API failures harder since Google often returns detailed error messages in the response body.🔧 Proposed fix to include response body in errors
- if (!res.ok) throw new Error(`Calendar API error: ${res.status}`); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Calendar API error: ${res.status} - ${err}`); + }Apply the same pattern to
createCalendarEvent,listDriveFiles, andcreateDriveFolder.Also applies to: 115-116, 127-128, 143-144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/lib/google-api.ts` around lines 98 - 99, Helper functions createCalendarEvent, listDriveFiles, and createDriveFolder throw errors only with the status code, causing inconsistent handling versus class methods like exchangeCode and refreshToken; update each helper to read the response body when res.ok is false (e.g., await res.text() or res.json() depending on content), and include that body in the thrown Error message alongside the status code and a clear prefix (e.g., "Calendar API error" / "Drive API error") so the error contains the server's detailed message for debugging.
119-129: Consider validatingfolderIdformat before query interpolation.The
folderIdis directly interpolated into the query string. While Google Drive IDs are typically alphanumeric, defensive validation would prevent potential query syntax issues if an unexpected value is passed.🛡️ Proposed defensive validation
export async function listDriveFiles(accessToken: string, folderId: string) { + // Google Drive IDs are alphanumeric with hyphens/underscores + if (!/^[\w-]+$/.test(folderId)) { + throw new Error('Invalid folder ID format'); + } const q = `'${folderId}' in parents and trashed = false`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/lib/google-api.ts` around lines 119 - 129, The listDriveFiles function currently interpolates folderId directly into the Drive query string (q), so add defensive validation for the folderId parameter: ensure folderId is non-empty and matches an expected Drive ID pattern (e.g., allowed characters like alphanumeric, hyphen, underscore) and reject/throw a descriptive error if it fails validation; alternatively sanitize/encode the value before building q to avoid breaking the query. Update listDriveFiles to perform this check on folderId and return/throw a clear error when validation fails so malformed IDs cannot be interpolated into q.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 498a4d4e-0100-43ac-b832-06faf4da3ebc
📒 Files selected for processing (34)
client/src/App.tsxclient/src/components/layout/Sidebar.tsxclient/src/components/property/AddLeaseDialog.tsxclient/src/components/property/AddUnitDialog.tsxclient/src/components/property/CommsPanel.tsxclient/src/components/property/EditPropertyDialog.tsxclient/src/components/property/GoogleCalendarEmbed.tsxclient/src/components/property/GoogleDriveEmbed.tsxclient/src/components/property/GoogleSheetsEmbed.tsxclient/src/components/property/PropertyDetailPanel.tsxclient/src/components/property/WorkflowBoard.tsxclient/src/components/property/WorkspaceTab.tsxclient/src/hooks/use-accounts.tsclient/src/hooks/use-integrations.tsclient/src/hooks/use-reports.tsclient/src/hooks/use-transactions.tsclient/src/pages/Accounts.tsxclient/src/pages/Connections.tsxclient/src/pages/Integrations.tsxclient/src/pages/PropertyDetail.tsxclient/src/pages/Reports.tsxclient/src/pages/Transactions.tsxdatabase/system.schema.tsserver/app.tsserver/env.tsserver/lib/google-api.tsserver/lib/sendgrid.tsserver/lib/twilio.tsserver/routes/accounts.tsserver/routes/comms.tsserver/routes/google.tsserver/routes/transactions.tsserver/routes/workflows.tsserver/storage/system.ts
Backend reads c.req.text() not c.req.formData(), so sending FormData wraps the CSV in boundary markers that fail to parse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewOverall this is a solid, well-structured PR. The code follows existing Hono/Drizzle patterns and the features are well-scoped. A few issues need attention before merging. Security Critical1. Workflow mutations lack tenant isolation In 2. Any authenticated user can supply arbitrary phone numbers or email addresses. Without validation the endpoint can send messages billed to your Twilio/SendGrid account. Validate Code Quality3.
4. Redundant ternary in
5.
6. Template invocation by positional spread is fragile In 7. Google OAuth callback references wrong state field
8. CSV export anchor not appended to DOM
Minor Suggestions9. No rate limiting on comms endpoints — 10. Workflow status not validated — What is well done
Items 1 and 2 (workflow tenant isolation, comms |
The legacy Express entry point (server/index.ts) imports Hono middleware that doesn't exist as Express middleware. Add no-op shims for chittyConnectAuth, serviceAuth, resolveTenant, and isAuthenticated so the dev server can start. Also add getSessionUser/getSessionContext to SystemStorage for legacy route compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes.ts (1)
1803-1803:⚠️ Potential issue | 🔴 CriticalSecurity: Mercury webhook endpoint is unprotected due to no-op
serviceAuth.The
serviceAuthshim on line 8 passes all requests unconditionally. This means the/api/integrations/mercury/webhookendpoint accepts unauthenticated POST requests, allowing attackers to:
- Inject fake financial events into your system
- Trigger downstream orchestration to evidence/ledger/chronicle services with malicious payloads
- Potentially corrupt transaction records
The real
serviceAuthmiddleware (see context snippet 2) validates Bearer tokens againstCHITTY_AUTH_SERVICE_TOKEN.🛡️ Proposed fix - inline validation for webhook
app.post("/api/integrations/mercury/webhook", serviceAuth, async (req: Request, res: Response) => { + // Fallback auth check if using legacy shims + const expected = process.env.CHITTY_AUTH_SERVICE_TOKEN; + if (expected) { + const auth = req.headers['authorization'] ?? ''; + const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; + if (!token || token !== expected) { + return res.status(401).json({ error: 'unauthorized' }); + } + } try {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/routes.ts` at line 1803, The route handler for POST "/api/integrations/mercury/webhook" currently uses a no-op serviceAuth shim that allows all requests; replace it by enforcing the real token check used elsewhere: update the serviceAuth middleware (or inline-check in the route) to read the Authorization header, require a Bearer token, compare it to process.env.CHITTY_AUTH_SERVICE_TOKEN, and return 401/403 for missing/invalid tokens before proceeding to the webhook logic so only requests with the configured service token are accepted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/routes.ts`:
- Around line 58-59: The isAuthenticated shim is a no-op causing the
/universal-connector/secured handler to receive undefined req.user and crash
when accessing req.user.claims.sub; replace the shim with real auth handling by
either wiring Express-compatible auth middleware that verifies the token and
populates req.user (e.g., decode/verify JWT or call the existing Hono auth
adapter) or, if temporarily needed, have isAuthenticated validate presence of an
authorization header and set req.user = { claims: { sub: <validated-sub> } }
before calling next(); ensure the middleware name is still isAuthenticated so
the secured route sees a populated req.user.claims.sub.
- Around line 5-9: The three no-op shim functions chittyConnectAuth,
resolveTenant, and serviceAuth completely bypass auth and tenant resolution;
replace the stubs with real checks or gate them behind an explicit dev flag:
implement service token validation in serviceAuth, populate req.tenantId (or
context c.set('tenantId')) in resolveTenant so downstream handlers like routes
using c.get('tenantId') get a tenant, and restore user identity checks in
chittyConnectAuth; if you must keep no-ops for local dev, require a boolean flag
(e.g. ALLOW_LEGACY_EXPRESS_SHIMS=true) and add a strong warning log, and throw
an error when NODE_ENV==='production' to prevent accidental deployment.
In `@server/storage/system.ts`:
- Around line 10-13: getSessionUser currently returns the first row from
schema.users with no session check; change it to require explicit session
context (e.g., accept a sessionId or userId parameter) and validate that
parameter against a session or authentication table before returning a user, or
else fail safely (return null or throw an authentication error). Locate
getSessionUser and replace the blind select on schema.users with a query that
joins/filters by the provided session token or user id (or rejects when no valid
session provided) and ensure callers are updated to pass the session context
instead of relying on a no-op shim.
---
Outside diff comments:
In `@server/routes.ts`:
- Line 1803: The route handler for POST "/api/integrations/mercury/webhook"
currently uses a no-op serviceAuth shim that allows all requests; replace it by
enforcing the real token check used elsewhere: update the serviceAuth middleware
(or inline-check in the route) to read the Authorization header, require a
Bearer token, compare it to process.env.CHITTY_AUTH_SERVICE_TOKEN, and return
401/403 for missing/invalid tokens before proceeding to the webhook logic so
only requests with the configured service token are accepted.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 71bbf307-12b5-444b-87ad-cf3a5b1272a8
📒 Files selected for processing (2)
server/routes.tsserver/storage/system.ts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
test |
CLAUDE.mdThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Canonical URI: Project OverviewChittyFinance is a full-stack financial management platform for the ChittyOS ecosystem. It provides intelligent financial tracking, AI-powered advice, recurring charge optimization, and integrations with Mercury Bank, Wave Accounting, and Stripe payments. Architecture: Hono on Cloudflare Workers (production) + legacy Express (local dev). PostgreSQL (Neon) with Drizzle ORM. React frontend. Essential CommandsDevelopmentnpm install # Install dependencies
npm run dev # Auto-detect mode and start dev server on port 5000
npm run dev:standalone # Start in standalone mode (local development)
npm run dev:system # Start in system mode (ChittyOS integration)
npm run check # TypeScript type checking
npm run mode:detect # Detect and display current mode
npm run mode:setup # Setup mode configuration (script not yet implemented)Build & Deploymentnpm run build # Build system mode (default for production)
npm run build:standalone # Build standalone mode (outputs to dist/standalone)
npm run build:system # Build system mode (outputs to dist/system)
npm run build:both # Build both modes
npm run start # Run standalone production build locally
npm run deploy # Deploy to Cloudflare Workers (system mode)
npm run deploy:staging # Deploy to staging environment
npm run deploy:production # Deploy to production environmentDatabase Operationsnpm run db:push # Push schema changes (uses current drizzle.config.ts)
npm run db:push:system # Push system schema to PostgreSQL
npm run db:push:standalone # Push standalone schema to SQLite
npm run db:seed # Seed IT CAN BE LLC entity structureFirst-Time Setup (System Mode): MODE=system npm run db:push:system # Create tables
npm run db:seed # Create tenants and usersFirst-Time Setup (Standalone Mode): npm run db:push:standalone # Create SQLite tables
# No seeding needed - single user modeCritical:
ArchitectureDual-Mode OperationChittyFinance supports two operational modes (controlled by Standalone Mode (default for local development):
System Mode (production - multi-tenant):
Mode Detection:
Multi-Tenant Architecture (System Mode)IT CAN BE LLC Entity Structure: Tenant Types:
Key Features:
Tech Stack
Project StructureDatabase ArchitectureSystem Mode Schema (Multi-Tenant PostgreSQL)Location: Core Tables:
Property Management Tables:
Supporting Tables:
Key Characteristics:
Standalone Mode Schema (Single-Tenant SQLite)Location: Simplified Tables:
Key Characteristics:
Database ConnectionMode-Aware Connection ( The database connection automatically switches based on System Mode (MODE=system): // PostgreSQL (Neon) with multi-tenant schema
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzleNeon({ client: pool, schema: systemSchema });Standalone Mode (MODE=standalone): // SQLite for local development
const sqlite = new Database('./chittyfinance.db');
const db = drizzleBetterSqlite(sqlite, { schema: standaloneSchema });Environment Variables:
Database Seeding (System Mode)Seed Script: Creates the complete IT CAN BE LLC entity structure:
Also creates:
Run seeding: npm run db:seedNote: Only run after pushing the system schema ( Storage Abstraction LayerCritical Pattern: All database access goes through
Interface ( export interface IStorage {
// User operations
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
// Integration operations
getIntegrations(userId: number): Promise<Integration[]>;
createIntegration(integration: InsertIntegration): Promise<Integration>;
updateIntegration(id: number, integration: Partial<Integration>): Promise<Integration | undefined>;
// Financial summary operations
getFinancialSummary(userId: number): Promise<FinancialSummary | undefined>;
createFinancialSummary(summary: InsertFinancialSummary): Promise<FinancialSummary>;
// Transaction, Task, and AI Message operations...
}Usage in routes ( import { storage } from "./storage";
const user = await storage.getUserByUsername("demo");
const summary = await storage.getFinancialSummary(user.id);Key Features1. Demo AuthenticationCurrent implementation: Auto-login as "demo" user (no real authentication). Pattern ( api.get("/session", async (req: Request, res: Response) => {
const user = await storage.getUserByUsername("demo");
// Returns demo user for all requests
});Note: All API routes assume demo user. Real authentication needs to be implemented for production. 2. Financial Dashboard
3. AI Financial AdviceLocation: Model: GPT-4o (hardcoded, comment at line 3 warns against changing) Functions:
API endpoint: 4. Recurring Charge AutomationLocation: Capabilities:
API endpoints:
5. Third-Party Integrations (Phase 3 - COMPLETED)Mercury Bank (
Wave Accounting (
Stripe (
DoorLoop (
GitHub (
Integration Status Monitoring:
Utilitiescheck_system_operations_duplicates.jsA utility script for analyzing and detecting duplicate operations in system mode. Located at the project root. This script helps maintain code quality when working with ChittyOS integration. Development WorkflowsAdding a New Feature
Working with AI FeaturesOpenAI Configuration ( const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const openai = OPENAI_API_KEY ? new OpenAI({ apiKey: OPENAI_API_KEY }) : null;Best practices:
Path AliasesConfigured in {
"@/*": ["./client/src/*"],
"@shared/*": ["./shared/*"]
}Additional alias in "@assets": path.resolve(import.meta.dirname, "attached_assets")Usage: import { Button } from "@/components/ui/button";
import { users } from "@shared/schema";
import logo from "@assets/logo.png";API EndpointsAuthentication
Financial Data
Integrations
Wave Accounting
Stripe
Mercury Bank
Recurring Charges
AI Services
GitHub Integration
Forensic Accounting
Tasks
Property CRUD (Phase 4)
Property Financial Data (Phase 4)
Property Valuation (Phase 4)
Data Import (Phase 4)
Environment ConfigurationRequired VariablesDatabase (required): DATABASE_URL="postgresql://user:pass@host/dbname"Application: NODE_ENV="development" # or "production"
MODE="standalone" # or "system" (multi-tenant)
PUBLIC_APP_BASE_URL="http://localhost:5000" # Base URL for OAuth redirectsOAuth Security (required for production): OAUTH_STATE_SECRET="random-secret-32chars" # HMAC secret for OAuth state tokensAI & OpenAI (optional for development, required for AI features): OPENAI_API_KEY="sk-..." # Required for AI financial adviceWave Accounting (Phase 3 - Real Integration): WAVE_CLIENT_ID="..." # OAuth client ID from Wave Developer Portal
WAVE_CLIENT_SECRET="..." # OAuth client secret
WAVE_REDIRECT_URI="http://localhost:5000/api/integrations/wave/callback" # Optional, defaults to PUBLIC_APP_BASE_URL/api/integrations/wave/callbackStripe (Phase 3 - Real Integration): STRIPE_SECRET_KEY="sk_test_..." # Stripe secret key (test or live)
STRIPE_PUBLISHABLE_KEY="pk_test_..." # Stripe publishable key (optional, frontend)
STRIPE_WEBHOOK_SECRET="whsec_..." # Webhook signing secret for verificationMercury Bank (Phase 3 - Real Integration via ChittyConnect): CHITTYCONNECT_API_BASE="https://connect.chitty.cc" # ChittyConnect backend URL
CHITTYCONNECT_API_TOKEN="..." # Service authentication token
CHITTY_CONNECT_URL="https://connect.chitty.cc" # Frontend redirect URL (optional)GitHub (optional): GITHUB_TOKEN="ghp_..." # Required for GitHub integrationProperty Valuation (Phase 4 - optional, each enables its provider): ZILLOW_API_KEY="..." # RapidAPI key for Zillow estimates
REDFIN_API_KEY="..." # RapidAPI key for Redfin estimates
HOUSECANARY_API_KEY="..." # HouseCanary API key
ATTOM_API_KEY="..." # ATTOM Data Gateway keyCook County Assessor (Socrata) is always available — no API key required. Local Development Setup
TestingManual Testing
Testing AI Features
Testing Integrations
Common Issues & SolutionsDatabase Connection ErrorsError: Solutions:
Port 5000 Already in UseError: Solution: lsof -ti:5000 | xargs kill -9Note: Port cannot be changed (hardcoded for Replit deployment). OpenAI API ErrorsError: 401 Unauthorized or 429 Rate Limit Solutions:
Demo User Not FoundError: Solution: Create demo user in database: INSERT INTO users (username, password, display_name, email, role)
VALUES ('demo', 'any_value', 'Demo User', 'demo@example.com', 'user');Type Checking FailuresError: TypeScript errors from Common causes:
Solution: Verify schema exports match usage, run ChittyOS Integration PointsChittyID Integration (Planned)
ChittyConnect Integration (Planned)
ChittyChronicle Integration (Planned)
Development Best PracticesDatabase Changes
API Design
Frontend Development
Security ConsiderationsOAuth Security (Phase 3 implemented):
Webhook Security:
Integration Validation (
General Security:
Known Limitations
Future EnhancementsPhase 1: Complete Multi-Tenant Implementation (COMPLETED)
Phase 1.5: Hono Route Migration (COMPLETED)
Phase 2: ChittyConnect Integration (Partially Completed)
Phase 3: Real Third-Party Integrations (COMPLETED ✅)
Phase 4: Property Financial API (Partially Completed)
Phase 5: ChittyOS Ecosystem Integration
Phase 6: Advanced Features
Related Documentation
|
ChittyFinanceA full‑stack TypeScript app combining an Express API and a React + Vite client. The server serves both API and client on port 5000. Quick Start
EnvironmentRequired:
Optional (features degrade gracefully if unset):
API Smoke Tests
DeployCloudflare Workers config is in
ChittyConnect (Mercury static IP + multi-account)Server uses ChittyConnect when configured to fetch Mercury data via static egress and supports selecting multiple bank accounts.
Note: The server bundle targets Node ESM. The Workers config enables |
|
{ |
|
testbody |
|
line1 |
|
#!/usr/bin/env node import fs from 'fs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function detectMode() { // Check for system mode indicators if (hasSystemDatabase || hasChittyIdToken || hasSystemConfig) { // Default to standalone const mode = detectMode(); // Write detected mode to environment // Set npm script to run export { detectMode }; |
CLAUDE.mdThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Canonical URI: Project OverviewChittyFinance is a full-stack financial management platform for the ChittyOS ecosystem. It provides intelligent financial tracking, AI-powered advice, recurring charge optimization, and integrations with Mercury Bank, Wave Accounting, and Stripe payments. Architecture: Hono on Cloudflare Workers (production) + legacy Express (local dev). PostgreSQL (Neon) with Drizzle ORM. React frontend. Essential CommandsDevelopmentnpm install # Install dependencies
npm run dev # Auto-detect mode and start dev server on port 5000
npm run dev:standalone # Start in standalone mode (local development)
npm run dev:system # Start in system mode (ChittyOS integration)
npm run check # TypeScript type checking
npm run mode:detect # Detect and display current mode
npm run mode:setup # Setup mode configuration (script not yet implemented)Build & Deploymentnpm run build # Build system mode (default for production)
npm run build:standalone # Build standalone mode (outputs to dist/standalone)
npm run build:system # Build system mode (outputs to dist/system)
npm run build:both # Build both modes
npm run start # Run standalone production build locally
npm run deploy # Deploy to Cloudflare Workers (system mode)
npm run deploy:staging # Deploy to staging environment
npm run deploy:production # Deploy to production environmentDatabase Operationsnpm run db:push # Push schema changes (uses current drizzle.config.ts)
npm run db:push:system # Push system schema to PostgreSQL
npm run db:push:standalone # Push standalone schema to SQLite
npm run db:seed # Seed IT CAN BE LLC entity structureFirst-Time Setup (System Mode): MODE=system npm run db:push:system # Create tables
npm run db:seed # Create tenants and usersFirst-Time Setup (Standalone Mode): npm run db:push:standalone # Create SQLite tables
# No seeding needed - single user modeCritical:
ArchitectureDual-Mode OperationChittyFinance supports two operational modes (controlled by Standalone Mode (default for local development):
System Mode (production - multi-tenant):
Mode Detection:
Multi-Tenant Architecture (System Mode)IT CAN BE LLC Entity Structure: Tenant Types:
Key Features:
Tech Stack
Project StructureDatabase ArchitectureSystem Mode Schema (Multi-Tenant PostgreSQL)Location: Core Tables:
Property Management Tables:
Supporting Tables:
Key Characteristics:
Standalone Mode Schema (Single-Tenant SQLite)Location: Simplified Tables:
Key Characteristics:
Database ConnectionMode-Aware Connection ( The database connection automatically switches based on System Mode (MODE=system): // PostgreSQL (Neon) with multi-tenant schema
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzleNeon({ client: pool, schema: systemSchema });Standalone Mode (MODE=standalone): // SQLite for local development
const sqlite = new Database('./chittyfinance.db');
const db = drizzleBetterSqlite(sqlite, { schema: standaloneSchema });Environment Variables:
Database Seeding (System Mode)Seed Script: Creates the complete IT CAN BE LLC entity structure:
Also creates:
Run seeding: npm run db:seedNote: Only run after pushing the system schema ( Storage Abstraction LayerCritical Pattern: All database access goes through
Interface ( export interface IStorage {
// User operations
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
// Integration operations
getIntegrations(userId: number): Promise<Integration[]>;
createIntegration(integration: InsertIntegration): Promise<Integration>;
updateIntegration(id: number, integration: Partial<Integration>): Promise<Integration | undefined>;
// Financial summary operations
getFinancialSummary(userId: number): Promise<FinancialSummary | undefined>;
createFinancialSummary(summary: InsertFinancialSummary): Promise<FinancialSummary>;
// Transaction, Task, and AI Message operations...
}Usage in routes ( import { storage } from "./storage";
const user = await storage.getUserByUsername("demo");
const summary = await storage.getFinancialSummary(user.id);Key Features1. Demo AuthenticationCurrent implementation: Auto-login as "demo" user (no real authentication). Pattern ( api.get("/session", async (req: Request, res: Response) => {
const user = await storage.getUserByUsername("demo");
// Returns demo user for all requests
});Note: All API routes assume demo user. Real authentication needs to be implemented for production. 2. Financial Dashboard
3. AI Financial AdviceLocation: Model: GPT-4o (hardcoded, comment at line 3 warns against changing) Functions:
API endpoint: 4. Recurring Charge AutomationLocation: Capabilities:
API endpoints:
5. Third-Party Integrations (Phase 3 - COMPLETED)Mercury Bank (
Wave Accounting (
Stripe (
DoorLoop (
GitHub (
Integration Status Monitoring:
Utilitiescheck_system_operations_duplicates.jsA utility script for analyzing and detecting duplicate operations in system mode. Located at the project root. This script helps maintain code quality when working with ChittyOS integration. Development WorkflowsAdding a New Feature
Working with AI FeaturesOpenAI Configuration ( const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const openai = OPENAI_API_KEY ? new OpenAI({ apiKey: OPENAI_API_KEY }) : null;Best practices:
Path AliasesConfigured in {
"@/*": ["./client/src/*"],
"@shared/*": ["./shared/*"]
}Additional alias in "@assets": path.resolve(import.meta.dirname, "attached_assets")Usage: import { Button } from "@/components/ui/button";
import { users } from "@shared/schema";
import logo from "@assets/logo.png";API EndpointsAuthentication
Financial Data
Integrations
Wave Accounting
Stripe
Mercury Bank
Recurring Charges
AI Services
GitHub Integration
Forensic Accounting
Tasks
Property CRUD (Phase 4)
Property Financial Data (Phase 4)
Property Valuation (Phase 4)
Data Import (Phase 4)
Environment ConfigurationRequired VariablesDatabase (required): DATABASE_URL="postgresql://user:pass@host/dbname"Application: NODE_ENV="development" # or "production"
MODE="standalone" # or "system" (multi-tenant)
PUBLIC_APP_BASE_URL="http://localhost:5000" # Base URL for OAuth redirectsOAuth Security (required for production): OAUTH_STATE_SECRET="random-secret-32chars" # HMAC secret for OAuth state tokensAI & OpenAI (optional for development, required for AI features): OPENAI_API_KEY="sk-..." # Required for AI financial adviceWave Accounting (Phase 3 - Real Integration): WAVE_CLIENT_ID="..." # OAuth client ID from Wave Developer Portal
WAVE_CLIENT_SECRET="..." # OAuth client secret
WAVE_REDIRECT_URI="http://localhost:5000/api/integrations/wave/callback" # Optional, defaults to PUBLIC_APP_BASE_URL/api/integrations/wave/callbackStripe (Phase 3 - Real Integration): STRIPE_SECRET_KEY="sk_test_..." # Stripe secret key (test or live)
STRIPE_PUBLISHABLE_KEY="pk_test_..." # Stripe publishable key (optional, frontend)
STRIPE_WEBHOOK_SECRET="whsec_..." # Webhook signing secret for verificationMercury Bank (Phase 3 - Real Integration via ChittyConnect): CHITTYCONNECT_API_BASE="https://connect.chitty.cc" # ChittyConnect backend URL
CHITTYCONNECT_API_TOKEN="..." # Service authentication token
CHITTY_CONNECT_URL="https://connect.chitty.cc" # Frontend redirect URL (optional)GitHub (optional): GITHUB_TOKEN="ghp_..." # Required for GitHub integrationProperty Valuation (Phase 4 - optional, each enables its provider): ZILLOW_API_KEY="..." # RapidAPI key for Zillow estimates
REDFIN_API_KEY="..." # RapidAPI key for Redfin estimates
HOUSECANARY_API_KEY="..." # HouseCanary API key
ATTOM_API_KEY="..." # ATTOM Data Gateway keyCook County Assessor (Socrata) is always available — no API key required. Local Development Setup
TestingManual Testing
Testing AI Features
Testing Integrations
Common Issues & SolutionsDatabase Connection ErrorsError: Solutions:
Port 5000 Already in UseError: Solution: lsof -ti:5000 | xargs kill -9Note: Port cannot be changed (hardcoded for Replit deployment). OpenAI API ErrorsError: 401 Unauthorized or 429 Rate Limit Solutions:
Demo User Not FoundError: Solution: Create demo user in database: INSERT INTO users (username, password, display_name, email, role)
VALUES ('demo', 'any_value', 'Demo User', 'demo@example.com', 'user');Type Checking FailuresError: TypeScript errors from Common causes:
Solution: Verify schema exports match usage, run ChittyOS Integration PointsChittyID Integration (Planned)
ChittyConnect Integration (Planned)
ChittyChronicle Integration (Planned)
Development Best PracticesDatabase Changes
API Design
Frontend Development
Security ConsiderationsOAuth Security (Phase 3 implemented):
Webhook Security:
Integration Validation (
General Security:
Known Limitations
Future EnhancementsPhase 1: Complete Multi-Tenant Implementation (COMPLETED)
Phase 1.5: Hono Route Migration (COMPLETED)
Phase 2: ChittyConnect Integration (Partially Completed)
Phase 3: Real Third-Party Integrations (COMPLETED ✅)
Phase 4: Property Financial API (Partially Completed)
Phase 5: ChittyOS Ecosystem Integration
Phase 6: Advanced Features
Related Documentation
|
|
Part 1 - Critical Issues. 1. Hardcoded OAuth fallback secret in server/routes/google.ts - falls back to a hardcoded string when OAUTH_STATE_SECRET is unset, silently degrading CSRF protection to zero. Apply the same 503 pattern used in Wave OAuth. 2. New tables commsLog and workflows are missing from database/standalone.schema.ts. Standalone mode will throw at runtime. Add equivalent tables or add 501 mode guards per route. |
CLAUDE.mdThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Canonical URI: Project OverviewChittyFinance is a full-stack financial management platform for the ChittyOS ecosystem. It provides intelligent financial tracking, AI-powered advice, recurring charge optimization, and integrations with Mercury Bank, Wave Accounting, and Stripe payments. Architecture: Hono on Cloudflare Workers (production) + legacy Express (local dev). PostgreSQL (Neon) with Drizzle ORM. React frontend. Essential CommandsDevelopmentnpm install # Install dependencies
npm run dev # Auto-detect mode and start dev server on port 5000
npm run dev:standalone # Start in standalone mode (local development)
npm run dev:system # Start in system mode (ChittyOS integration)
npm run check # TypeScript type checking
npm run mode:detect # Detect and display current mode
npm run mode:setup # Setup mode configuration (script not yet implemented)Build & Deploymentnpm run build # Build system mode (default for production)
npm run build:standalone # Build standalone mode (outputs to dist/standalone)
npm run build:system # Build system mode (outputs to dist/system)
npm run build:both # Build both modes
npm run start # Run standalone production build locally
npm run deploy # Deploy to Cloudflare Workers (system mode)
npm run deploy:staging # Deploy to staging environment
npm run deploy:production # Deploy to production environmentDatabase Operationsnpm run db:push # Push schema changes (uses current drizzle.config.ts)
npm run db:push:system # Push system schema to PostgreSQL
npm run db:push:standalone # Push standalone schema to SQLite
npm run db:seed # Seed IT CAN BE LLC entity structureFirst-Time Setup (System Mode): MODE=system npm run db:push:system # Create tables
npm run db:seed # Create tenants and usersFirst-Time Setup (Standalone Mode): npm run db:push:standalone # Create SQLite tables
# No seeding needed - single user modeCritical:
ArchitectureDual-Mode OperationChittyFinance supports two operational modes (controlled by Standalone Mode (default for local development):
System Mode (production - multi-tenant):
Mode Detection:
Multi-Tenant Architecture (System Mode)IT CAN BE LLC Entity Structure: Tenant Types:
Key Features:
Tech Stack
Project StructureDatabase ArchitectureSystem Mode Schema (Multi-Tenant PostgreSQL)Location: Core Tables:
Property Management Tables:
Supporting Tables:
Key Characteristics:
Standalone Mode Schema (Single-Tenant SQLite)Location: Simplified Tables:
Key Characteristics:
Database ConnectionMode-Aware Connection ( The database connection automatically switches based on System Mode (MODE=system): // PostgreSQL (Neon) with multi-tenant schema
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzleNeon({ client: pool, schema: systemSchema });Standalone Mode (MODE=standalone): // SQLite for local development
const sqlite = new Database('./chittyfinance.db');
const db = drizzleBetterSqlite(sqlite, { schema: standaloneSchema });Environment Variables:
Database Seeding (System Mode)Seed Script: Creates the complete IT CAN BE LLC entity structure:
Also creates:
Run seeding: npm run db:seedNote: Only run after pushing the system schema ( Storage Abstraction LayerCritical Pattern: All database access goes through
Interface ( export interface IStorage {
// User operations
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
// Integration operations
getIntegrations(userId: number): Promise<Integration[]>;
createIntegration(integration: InsertIntegration): Promise<Integration>;
updateIntegration(id: number, integration: Partial<Integration>): Promise<Integration | undefined>;
// Financial summary operations
getFinancialSummary(userId: number): Promise<FinancialSummary | undefined>;
createFinancialSummary(summary: InsertFinancialSummary): Promise<FinancialSummary>;
// Transaction, Task, and AI Message operations...
}Usage in routes ( import { storage } from "./storage";
const user = await storage.getUserByUsername("demo");
const summary = await storage.getFinancialSummary(user.id);Key Features1. Demo AuthenticationCurrent implementation: Auto-login as "demo" user (no real authentication). Pattern ( api.get("/session", async (req: Request, res: Response) => {
const user = await storage.getUserByUsername("demo");
// Returns demo user for all requests
});Note: All API routes assume demo user. Real authentication needs to be implemented for production. 2. Financial Dashboard
3. AI Financial AdviceLocation: Model: GPT-4o (hardcoded, comment at line 3 warns against changing) Functions:
API endpoint: 4. Recurring Charge AutomationLocation: Capabilities:
API endpoints:
5. Third-Party Integrations (Phase 3 - COMPLETED)Mercury Bank (
Wave Accounting (
Stripe (
DoorLoop (
GitHub (
Integration Status Monitoring:
Utilitiescheck_system_operations_duplicates.jsA utility script for analyzing and detecting duplicate operations in system mode. Located at the project root. This script helps maintain code quality when working with ChittyOS integration. Development WorkflowsAdding a New Feature
Working with AI FeaturesOpenAI Configuration ( const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const openai = OPENAI_API_KEY ? new OpenAI({ apiKey: OPENAI_API_KEY }) : null;Best practices:
Path AliasesConfigured in {
"@/*": ["./client/src/*"],
"@shared/*": ["./shared/*"]
}Additional alias in "@assets": path.resolve(import.meta.dirname, "attached_assets")Usage: import { Button } from "@/components/ui/button";
import { users } from "@shared/schema";
import logo from "@assets/logo.png";API EndpointsAuthentication
Financial Data
Integrations
Wave Accounting
Stripe
Mercury Bank
Recurring Charges
AI Services
GitHub Integration
Forensic Accounting
Tasks
Property CRUD (Phase 4)
Property Financial Data (Phase 4)
Property Valuation (Phase 4)
Data Import (Phase 4)
Environment ConfigurationRequired VariablesDatabase (required): DATABASE_URL="postgresql://user:pass@host/dbname"Application: NODE_ENV="development" # or "production"
MODE="standalone" # or "system" (multi-tenant)
PUBLIC_APP_BASE_URL="http://localhost:5000" # Base URL for OAuth redirectsOAuth Security (required for production): OAUTH_STATE_SECRET="random-secret-32chars" # HMAC secret for OAuth state tokensAI & OpenAI (optional for development, required for AI features): OPENAI_API_KEY="sk-..." # Required for AI financial adviceWave Accounting (Phase 3 - Real Integration): WAVE_CLIENT_ID="..." # OAuth client ID from Wave Developer Portal
WAVE_CLIENT_SECRET="..." # OAuth client secret
WAVE_REDIRECT_URI="http://localhost:5000/api/integrations/wave/callback" # Optional, defaults to PUBLIC_APP_BASE_URL/api/integrations/wave/callbackStripe (Phase 3 - Real Integration): STRIPE_SECRET_KEY="sk_test_..." # Stripe secret key (test or live)
STRIPE_PUBLISHABLE_KEY="pk_test_..." # Stripe publishable key (optional, frontend)
STRIPE_WEBHOOK_SECRET="whsec_..." # Webhook signing secret for verificationMercury Bank (Phase 3 - Real Integration via ChittyConnect): CHITTYCONNECT_API_BASE="https://connect.chitty.cc" # ChittyConnect backend URL
CHITTYCONNECT_API_TOKEN="..." # Service authentication token
CHITTY_CONNECT_URL="https://connect.chitty.cc" # Frontend redirect URL (optional)GitHub (optional): GITHUB_TOKEN="ghp_..." # Required for GitHub integrationProperty Valuation (Phase 4 - optional, each enables its provider): ZILLOW_API_KEY="..." # RapidAPI key for Zillow estimates
REDFIN_API_KEY="..." # RapidAPI key for Redfin estimates
HOUSECANARY_API_KEY="..." # HouseCanary API key
ATTOM_API_KEY="..." # ATTOM Data Gateway keyCook County Assessor (Socrata) is always available — no API key required. Local Development Setup
TestingManual Testing
Testing AI Features
Testing Integrations
Common Issues & SolutionsDatabase Connection ErrorsError: Solutions:
Port 5000 Already in UseError: Solution: lsof -ti:5000 | xargs kill -9Note: Port cannot be changed (hardcoded for Replit deployment). OpenAI API ErrorsError: 401 Unauthorized or 429 Rate Limit Solutions:
Demo User Not FoundError: Solution: Create demo user in database: INSERT INTO users (username, password, display_name, email, role)
VALUES ('demo', 'any_value', 'Demo User', 'demo@example.com', 'user');Type Checking FailuresError: TypeScript errors from Common causes:
Solution: Verify schema exports match usage, run ChittyOS Integration PointsChittyID Integration (Planned)
ChittyConnect Integration (Planned)
ChittyChronicle Integration (Planned)
Development Best PracticesDatabase Changes
API Design
Frontend Development
Security ConsiderationsOAuth Security (Phase 3 implemented):
Webhook Security:
Integration Validation (
General Security:
Known Limitations
Future EnhancementsPhase 1: Complete Multi-Tenant Implementation (COMPLETED)
Phase 1.5: Hono Route Migration (COMPLETED)
Phase 2: ChittyConnect Integration (Partially Completed)
Phase 3: Real Third-Party Integrations (COMPLETED ✅)
Phase 4: Property Financial API (Partially Completed)
Phase 5: ChittyOS Ecosystem Integration
Phase 6: Advanced Features
Related Documentation
|
|
FULL CODE REVIEW - Please see the detailed review below (posted in sections due to formatting constraints). OVERVIEW: This is a large PR (3,979 additions) covering four new frontend pages, a property management component suite, three new backend route modules, new schema tables, and new service integrations. The work is substantive and follows many established patterns, but there are several issues worth addressing before merge. |
|
Critical Issues: 1. Hardcoded OAuth fallback secret - The Google OAuth handler falls back to a hardcoded string when OAUTH_STATE_SECRET is unset. Apply the 503 pattern from Wave OAuth. 2. Standalone schema gap - commsLog and workflows missing from standalone.schema.ts. Security Issues: 3. Missing Zod validation on /api/comms/send and /api/comms/template. 4. No role check on workflow approve/complete mutations. 5. No rate limiting on comms endpoints. 6. Tenant isolation gap - propertyId not validated against tenantId. 7. Template arity not validated in comms handler. Code Quality: 8. No zValidator on POST/PATCH endpoints. 9. JSONB unsafe casts. 10. Client-side pagination. 11. CSV memory usage. 12. Hardcoded Chicago timezone. Architecture: 13. Express auth shims are no-ops. 14. No idempotency on POST endpoints. 15. Google refresh token plaintext. Positives: Clean TanStack Query hooks, edge-compatible API clients, correct OAuth callback mounting, tenantId scoping in updateTransaction, 503 on missing env vars, 135 tests passing. Priority: Must fix - items 1 and 2. Should fix - items 3-10. Nice to have - items 11-15. |
Code Review: feat: core bookkeeping UI + property managementThis is a large PR (3,979 additions) covering four new frontend pages, a property management component suite, three new backend route modules, new schema tables, and new service integrations. The work is substantive and follows many established patterns, but there are several issues worth addressing before merge. Critical Issues 1. Hardcoded OAuth fallback secret (server/routes/google.ts) The Google OAuth handler uses a fallback that silently degrades CSRF protection to zero if the env var is not set. The Wave OAuth in this codebase returns 503 when secrets are missing — apply the same here: check for the secret and return 503 if it is missing rather than falling back to a hardcoded string. 2. New tables missing from standalone schema (database/standalone.schema.ts) commsLog and workflows are defined only in database/system.schema.ts. Standalone mode will throw at runtime when any of the new routes are hit. Either add equivalent tables to standalone.schema.ts, or add explicit mode guards in each route returning 501 Not Implemented for standalone mode. Security Issues 3. Missing input validation on comms routes POST /api/comms/send and POST /api/comms/template accept to, message, recipientName with no Zod validation. The codebase uses zValidator in other routes — this should be consistent. At minimum: email format validation, E.164 phone format, and message length cap. 4. No authorization on workflow mutations PATCH /api/workflows/:id/approve and PATCH /api/workflows/:id/complete do not verify the requesting user has permission to approve. Any authenticated user in a tenant can approve any workflow. Add a role check (accountant or admin required for approvals). 5. No rate limiting on SMS/email endpoints POST /api/comms/send can be called in a tight loop, racking up Twilio/SendGrid costs. Add per-tenant rate limiting or a daily cap enforced via a count query on the commsLog table. 6. Tenant isolation gap in comms/workflows When fetching or mutating by propertyId, there is no check that the property belongs to the current tenantId. A user in Tenant A could supply Tenant B's propertyId. Validate property ownership before operating. 7. Template injection in comms The comms template handler calls the template function with Object.values(params) without validating that the arg count matches the template arity. This silently produces malformed messages. Add a per-template parameter schema. Code Quality 8. No Zod schemas on new POST/PATCH endpoints POST /api/transactions, POST /api/accounts, POST /api/comms/send, POST /api/workflows all use manual if (!body.field) guards. Existing routes use zValidator middleware — apply it here for consistency. 9. Unsafe type casts on JSONB fields Several routes cast credentials and metadata JSONB columns directly to Record without runtime validation. Use a Zod schema or type guard before accessing nested properties. 10. In-memory filtering and pagination Transactions.tsx and Accounts.tsx load all records and filter/paginate client-side. This degrades at realistic dataset sizes. GET /api/transactions should accept page, limit, category, accountId as query params and paginate server-side. 11. CSV export memory usage The CSV escaping logic is correct, but the entire dataset is materialized in memory. For large exports, stream the response or enforce a row cap with a user-visible warning. 12. Google Calendar hardcoded timezone The Google Calendar embed uses ctz=America/Chicago. This will confuse non-Chicago tenants. Derive the timezone from the property address or a user preference. Architecture / Conventions 13. Legacy Express auth shims in server/routes.ts All auth middleware in the legacy Express routes is stubbed as a no-op. This is pre-existing, but worth flagging since this PR adds meaningful new functionality that is still Express-backed in standalone mode, meaning no auth in that path. 14. Missing idempotency on POST endpoints POST /api/transactions, POST /api/accounts, and POST /api/workflows will create duplicates on network retry. A client-supplied idempotency key (or at minimum documentation) would prevent silent duplication. 15. Google refresh token stored in plaintext The OAuth refresh token is stored in integrations.credentials JSONB. Neon encrypts at rest, which is acceptable, but consider encrypting the value with OAUTH_STATE_SECRET before storing so it is protected if rows are exported or logged. Positive Notes
Summary
|
|
line1 |
Recurring charges are now detected by analyzing the tenant's own transaction history — grouping expenses by payee, identifying repeat charges with similar amounts (10% tolerance), and classifying frequency (monthly/quarterly/annual/irregular). Replaces the stub fetchChargesFromIntegration() that always returned [] with detectRecurringCharges() that scans real transaction data. Optimization recommendations now use detected patterns: consolidation for duplicate-category charges, negotiation for high-cost monthly charges, downgrade for software subscriptions, and cancellation review for irregular low-occurrence charges. Frontend updated: frequency badge column, occurrence count per merchant, updated empty state message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ysis
The legacy chargeAutomation.ts had 6 fetch*Charges() functions that all
returned empty arrays with console.warn("not yet implemented"). Now uses
the same transaction pattern analysis as the Hono charges route —
detecting recurring expenses by payee grouping, amount consistency, and
interval classification.
manageRecurringCharge() now flags transactions via metadata instead of
returning a hardcoded failure.
Zero stubs remain in the codebase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: feat/core-bookkeeping-uiThis is a substantial PR adding four major pages (Transactions, Accounts, Reports, Integrations) plus Google Workspace, Twilio/SendGrid comms, and workflow management. The frontend work is clean and well-structured. Here are my findings organized by priority. 🔴 Critical / Security1. Authentication shims bypass all auth in legacy Express routes ( The PR adds no-op shims for const chittyConnectAuth = (_req: any, _res: any, next: any) => next();
const serviceAuth = (_req: any, _res: any, next: any) => next();
const resolveTenant = (_req: any, _res: any, next: any) => next();
const isAuthenticated = (_req: any, _res: any, next: any) => next();While labeled "legacy Express compat shims for dev server startup," there's no environment guard preventing these from being active in production. Any Express route protected by these middleware is now completely unauthenticated. If the legacy Express server is ever deployed, this is an open door. At minimum, add 2. The endpoint sends real SMS/email via Twilio/SendGrid on every request with no rate limit, no per-tenant cap, and no recipient validation. A single authenticated user can trigger unlimited outbound messages, burning through provider budgets. Add rate limiting middleware (e.g., Hono's built-in limiter or a KV-backed counter). 3. Phone number and email not validated before sending ( The 4. Potential HTML injection in email templates ( Template functions like html: `<p>Hi ${tenantName},</p><p>Your lease at <strong>${propertyName}</strong>...`If 5. Drive query string injection ( const q = `'${folderId}' in parents and trashed = false`;
6. const workflow = await storage.getWorkflow(id);
// then update without checking tenantId
🟡 Code Quality7. Duplicate recurring charge detection logic
8. Pervasive transactions = await (storage as any).getTransactions(String(userId));
transactions = (storage as any).getTransactionsByUser...
await (storage as any).updateTransaction(chargeId, ...)The storage abstraction layer should be typed. The 9. Dead code in message: channel === 'email' ? message : message,Both branches are identical. This is harmless but suggests the email body was meant to be HTML-wrapped or formatted differently for email vs. SMS. 10. Median calculation mutates input array ( const median = amounts.sort((a, b) => a - b)[Math.floor(amounts.length / 2)];
11. async getSessionUser() {
const [user] = await this.db.select().from(schema.users).limit(1);
return user;
}This is documented as a legacy compat shim, but returning the first user in the table is non-deterministic — the result depends on insertion order, database row storage, and query plan. This will silently return the wrong user if user row ordering ever changes. Consider at least ordering by 12. New routes missing Zod input validation The existing codebase uses Zod for input validation (e.g., 🟠 Performance13. Full transaction table scan on every const transactions = await storage.getTransactions(tenantId);
const charges = detectRecurringCharges(transactions);
✅ Positives
Test Plan GapsThe PR test plan omits the new backend routes. Before merging:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@client/src/hooks/use-integrations.ts`:
- Around line 74-93: The useTurboTenantImport mutation currently builds a raw
fetch URL that becomes ?tenantId=null and bypasses the withTenant/apiRequest
pattern; change useTurboTenantImport to construct the request using the same
tenant-aware helper as useWaveSync (or call withTenant(tenantId,
'/api/import/turbotenant') to only append tenantId when non-null) and send the
CSV body via fetch while preserving custom headers (X-Account-ID and
Content-Type); ensure you explicitly omit the tenantId query parameter when
tenantId is null/undefined and keep the onSuccess invalidation logic targeting
['/api/transactions', tenantId] so the hook remains consistent with
apiRequest-driven flows.
In `@server/routes/charges.ts`:
- Around line 147-166: The consolidation block is creating duplicate
recommendations per charge in a category; update the logic in the for loop that
iterates over charges (the block referencing categoryGroups, sameCategory, and
recommendations.push) so you only emit one "consolidate" recommendation per
category (e.g., only when the current charge is the first in sameCategory —
compare charge.id to sameCategory[0].id or track a handledCategories Set) and
skip creating recommendations for the other charges in that category; keep the
totalInCategory and potentialSavings calculation but only add it once and still
continue to avoid double-recommending.
- Around line 237-268: Add Zod input validation for the /api/charges/manage
endpoint: create a Zod schema (e.g., ManageChargeSchema) to parse and validate {
chargeId: string, action: enum('cancel','downgrade','consolidate','negotiate') }
and replace the raw c.req.json() + manual checks in
chargeRoutes.post('/api/charges/manage') with schema.parseAsync(c.req.json()).
For the metadata merge, do not overwrite existing metadata in
storage.updateTransaction; instead fetch the existing transaction first—either
add and call storage.getTransaction(chargeId, tenantId) (preferred) or, if you
cannot add that API, locate the transaction via getTransactionsByAccount and
filter by id—then shallow-merge existing.metadata with { chargeAction: action,
flaggedAt: new Date().toISOString() } and pass the merged metadata to
storage.updateTransaction; update any storage interface types you change to
include getTransaction if you add it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b53067f7-458b-4cb0-b863-84d9cd7915ef
📒 Files selected for processing (3)
client/src/hooks/use-integrations.tsclient/src/pages/Integrations.tsxserver/routes/charges.ts
|
Code Review PR 46 - Core Bookkeeping UI and Property Management |
|
CRITICAL - Must Fix
All four auth/tenant middleware functions (chittyConnectAuth, resolveTenant, serviceAuth, isAuthenticated) are replaced with no-ops that call next() unconditionally. This makes every route in routes.ts unauthenticated in the Express standalone path. The comment says Hono middleware cannot be used as Express middleware, but the original imports were Express-compatible middleware from server/middleware/auth.ts. These should either continue to work as-is or the standalone route file needs proper Express-native guards reinstated.
SystemStorage.updateWorkflow and getWorkflow never filter by tenantId. Compare with the correct pattern already used in updateTransaction: .where(and(eq(transactions.id, id), eq(transactions.tenantId, tenantId))). The workflow PATCH routes must pass tenantId down and storage must add a tenant condition to both the lookup and update queries. |
|
HIGH - Should Fix
In server/routes/google.ts, every API call uses creds.access_token directly. The expires_at field is stored during OAuth but never checked. Once the access token expires (~1 hour), all Google API calls silently fail with 401s. Add a helper that checks expires_at, calls GoogleAPIClient.refreshToken() if expired, persists the new token, and returns a fresh access token before proceeding.
The expression Math.abs(a - median) / median produces NaN/Infinity when median equals 0. If a payee group has a median charge of zero, consistent will be empty and valid data is silently dropped. Add if (median === 0) continue before the consistent check.
The method queries users.limit(1) which returns whichever user sorts first in the DB. Add a NODE_ENV !== production guard and a comment that this must never be used in multi-tenant production paths. |
|
MEDIUM - Nice to Fix
function formatTransaction(t: any) should use typeof schema.transactions.$inferSelect to catch type mismatches at compile time, consistent with the project TypeScript-first conventions.
title is notNull() in the schema but not validated in the route handler — the request hits a DB constraint error and returns 500 instead of a clean 400. type also accepts arbitrary strings with no allowlist. Add explicit 400 guards and a VALID_TYPES allowlist matching the pattern in POST /api/accounts.
googleapis.com/auth/calendar grants full read/write access; calendar.events is sufficient for event creation. googleapis.com/auth/spreadsheets grants full access to all user sheets; spreadsheets.readonly is sufficient if Sheets is only embedded. Narrower scopes reduce blast radius if tokens leak.
Object.values(params) order depends on key insertion order, so callers must send keys in exactly the right sequence. Missing or extra keys silently produce wrong messages. Consider documenting required key order per template name, or accept a typed params object keyed by template name. |
|
Positive Notes
Pre-merge checklist:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/lib/chargeAutomation.ts`:
- Around line 116-123: The code currently swallows any error from
storage.getTransactions() and treats it as "no transactions"; instead, only use
the fallback when getTransactions is not available — not when it fails
unexpectedly. Change the logic so you first check if (storage as
any).getTransactions is a function and call it; if it's not a function, call
getTransactionsByUser if present; but if getTransactions exists and throws,
rethrow or let the error propagate (do not convert to []). Refer to storage,
getTransactions, and getTransactionsByUser to implement this behavior.
- Around line 57-101: The code computes a filtered numeric array named
`consistent` but continues to use the full `group` for cadence and occurrence
logic; change this so all cadence calculations use the filtered transaction
objects instead. Create a `consistentGroup` by filtering `group` to the
transactions whose absolute amounts match the `consistent` criterion (or
recompute `median` from `consistent` then filter by that), then compute `dates`,
`intervals`, `avgInterval`, determine `frequency`, set `latest` (most recent in
`consistentGroup`), `occurrences` (use `consistentGroup.length`), and
`nextChargeDate` from `consistentGroup` instead of `group` before pushing into
`recurring`.
- Around line 141-156: The current loop over charges emits a consolidation
recommendation for every charge in the same category, causing duplicates; fix
this by emitting one recommendation per category instead—either iterate
categoryGroups.entries() (or use a Set emittedCategories) and when
sameCategory.length > 1 push a single recommendation (use a representative
charge from sameCategory, e.g. sameCategory[0] for
merchantName/chargeId/currentAmount) with
potentialSavings/reasoning/alternativeOptions built from the whole sameCategory,
then skip per-charge pushing; update references to categoryGroups, charges,
recommendations, sameCategory and the 'consolidate' suggestedAction accordingly.
- Around line 205-211: The code calls storage.updateTransaction with three args
unconditionally and returns success even when persistence isn't available;
change the branch to detect the updateTransaction signature and call it
correctly: check (storage as any).updateTransaction.length === 3 and call
updateTransaction(chargeId, String(_userId), { metadata: ... }) for the system
signature, otherwise call updateTransaction(chargeId, { metadata: ... }) for
standalone; if updateTransaction is not present, return success: false with a
clear message. Ensure you reference the existing symbols updateTransaction,
storage, chargeId and _userId when making the fix.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2fe09600-c205-4e10-b129-d96b045990fd
📒 Files selected for processing (1)
server/lib/chargeAutomation.ts
Summary
/api/reports/consolidated, by-entity and by-state breakdowns, data quality metrics (uncategorized, unreconciled, no-state, future-dated), tax readiness checklist with pass/warn/fail badges, tax automation trigger with AI reviewPOST /api/transactions,PATCH /api/transactions/:id,POST /api/accounts,SystemStorage.updateTransaction()Test plan
npx tsc --noEmit— cleannpx vitest run— 135/135 tests passing🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
API & Services
Chores