diff --git a/client/src/App.tsx b/client/src/App.tsx index de9da4c..1883332 100755 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,6 +13,7 @@ import Properties from "@/pages/Properties"; import Sidebar from "@/components/layout/Sidebar"; import Header from "@/components/layout/Header"; import Connections from "@/pages/Connections"; +import Dashboard from "@/pages/Dashboard"; import { User } from "@shared/schema"; import { ThemeProvider } from "@/contexts/ThemeContext"; import { TenantProvider } from "@/contexts/TenantContext"; @@ -31,6 +32,7 @@ function Router() {
+ diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index b368ba7..b9524e8 100755 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -1,9 +1,21 @@ import { Bell, Search, Command } from "lucide-react"; import { useRole } from "@/contexts/RoleContext"; +import { useLocation } from "wouter"; import { TenantSwitcher } from "./TenantSwitcher"; +const PAGE_LABELS: Record = { + '/': 'Portfolio', + '/dashboard': 'Dashboard', + '/connections': 'Connections', + '/admin': 'Admin', + '/settings': 'Settings', +}; + export default function Header() { const { currentRole, roleConfig } = useRole(); + const [location] = useLocation(); + const pageLabel = PAGE_LABELS[location] || (location.startsWith('/properties/') ? 'Property Detail' : 'Overview'); + return (
{/* Tenant Switcher / Breadcrumb */} @@ -11,7 +23,7 @@ export default function Header() { / - Overview + {pageLabel} diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx index fa88d49..76d0a26 100755 --- a/client/src/components/layout/Sidebar.tsx +++ b/client/src/components/layout/Sidebar.tsx @@ -1,10 +1,9 @@ import { Link, useLocation } from "wouter"; import { cn } from "@/lib/utils"; import { - BarChart3, ArrowLeftRight, FileText, Receipt, - Settings, Plug, Building2, Users, BookOpen, Calculator, Shield, - ChevronDown, ChevronRight, ChevronsUpDown, Wallet, - Menu, X, Activity + Settings, Plug, Building2, Shield, + ChevronDown, ChevronRight, ChevronsUpDown, + Menu, X, Activity, LayoutDashboard } from "lucide-react"; import { useState, useMemo } from "react"; import { useRole, type UserRole } from "@/contexts/RoleContext"; @@ -21,14 +20,7 @@ interface NavItem { const NAV_ITEMS: NavItem[] = [ { href: "/", label: "Portfolio", icon: Building2, roles: ["cfo", "accountant", "bookkeeper", "user"] }, - { href: "/accounts", label: "Accounts", icon: Wallet, roles: ["cfo", "accountant", "bookkeeper"] }, - { href: "/transactions", label: "Transactions", icon: ArrowLeftRight, roles: ["cfo", "accountant", "bookkeeper", "user"] }, - { href: "/reports", label: "Reports", icon: BarChart3, roles: ["cfo", "accountant"] }, - { href: "/reconciliation", label: "Reconciliation", icon: Calculator, roles: ["accountant", "bookkeeper"] }, - { href: "/invoices", label: "Invoices", icon: FileText, roles: ["cfo", "accountant", "bookkeeper"] }, - { href: "/expenses", label: "Expenses", icon: Receipt, roles: ["user"] }, - { href: "/journal", label: "Journal", icon: BookOpen, roles: ["accountant"] }, - { href: "/team", label: "Team", icon: Users, roles: ["cfo"] }, + { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard, roles: ["cfo", "accountant", "bookkeeper", "user"] }, { href: "/connections", label: "Connections", icon: Plug, roles: ["cfo", "accountant"] }, { href: "/admin", label: "Admin", icon: Shield, roles: ["cfo"] }, { href: "/settings", label: "Settings", icon: Settings, roles: ["cfo", "accountant", "bookkeeper", "user"] }, @@ -196,7 +188,7 @@ export default function Sidebar() { const [location] = useLocation(); const [mobileOpen, setMobileOpen] = useState(false); const { currentRole } = useRole(); - const { currentTenant, tenants, switchTenant, isSystemMode } = useTenant(); + const { currentTenant, tenants, switchTenant } = useTenant(); const [entityExpanded, setEntityExpanded] = useState(true); const entityTree = useMemo(() => buildEntityTree(tenants), [tenants]); diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts index d9afb6e..1dd3420 100755 --- a/client/src/lib/utils.ts +++ b/client/src/lib/utils.ts @@ -70,7 +70,6 @@ export function getServiceIcon(serviceType: string): React.ReactNode { const iconMap: Record = { 'mercury_bank': 'M', 'wavapps': 'W', - 'doorloop': 'D', 'stripe': 'S', 'quickbooks': 'Q', 'xero': 'X', @@ -87,7 +86,6 @@ export function getServiceColor(serviceType: string): string { const colorMap: Record = { 'mercury_bank': 'bg-blue-500', 'wavapps': 'bg-teal-500', - 'doorloop': 'bg-orange-500', 'stripe': 'bg-purple-500', 'quickbooks': 'bg-green-500', 'xero': 'bg-blue-400', diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 6f4dc0a..4f05f83 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -33,7 +33,6 @@ const SERVICES: ServiceHealth[] = [ { name: "ChittyCommand", url: "command.chitty.cc", status: "healthy", latency: 67, lastCheck: "1m ago", version: "3.1.0" }, { name: "ChittyRegister", url: "register.chitty.cc", status: "healthy", latency: 31, lastCheck: "1m ago", version: "1.0.5" }, { name: "ChittyRouter", url: "router.chitty.cc", status: "degraded", latency: 234, lastCheck: "2m ago", version: "1.1.0" }, - { name: "DoorLoop", url: "api.doorloop.com", status: "unknown", lastCheck: "—" }, { name: "Wave Accounting", url: "api.waveapps.com", status: "healthy", latency: 156, lastCheck: "5m ago" }, ]; @@ -51,7 +50,6 @@ const INTEGRATIONS: IntegrationConfig[] = [ { name: "Wave Accounting", configured: true, envVars: ["WAVE_CLIENT_ID", "WAVE_CLIENT_SECRET"], status: "active", lastSync: "1h ago" }, { name: "Stripe Payments", configured: true, envVars: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"], status: "active", lastSync: "5m ago" }, { name: "OpenAI (GPT-4o)", configured: true, envVars: ["OPENAI_API_KEY"], status: "active" }, - { name: "DoorLoop", configured: false, envVars: ["DOORLOOP_API_KEY"], status: "inactive" }, { name: "GitHub", configured: true, envVars: ["GITHUB_TOKEN"], status: "active" }, ]; diff --git a/client/src/pages/ConnectAccounts.tsx b/client/src/pages/ConnectAccounts.tsx index eb5ba99..2a376eb 100755 --- a/client/src/pages/ConnectAccounts.tsx +++ b/client/src/pages/ConnectAccounts.tsx @@ -7,7 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useToast } from "@/hooks/use-toast"; import { useLocation } from "wouter"; import { apiRequest } from "@/lib/queryClient"; -import { Building, CreditCard, DollarSign, BarChart4, Home, CreditCard as CreditCardIcon } from "lucide-react"; +import { Building, CreditCard, DollarSign, BarChart4, CreditCard as CreditCardIcon } from "lucide-react"; import { AuthContext } from "../App"; // Service connection card component @@ -54,7 +54,7 @@ export default function ConnectAccounts() { const [loading, setLoading] = useState(false); const [apiKey, setApiKey] = useState(""); const [connectedServices, setConnectedServices] = useState([ - "mercury_bank", "doorloop" // Default connected services + "mercury_bank" // Default connected services ]); // Redirect to login if not authenticated @@ -124,7 +124,6 @@ export default function ConnectAccounts() { "quickbooks": "QuickBooks", "xero": "Xero Accounting", "wavapps": "WavApps", - "doorloop": "DoorLoop", "brex": "Brex", "gusto": "Gusto Payroll" }; @@ -138,7 +137,6 @@ export default function ConnectAccounts() { "quickbooks": "Accounting Software", "xero": "Global Accounting Platform", "wavapps": "Financial Software", - "doorloop": "Property Management", "brex": "Business Credit & Expenses", "gusto": "Payroll & HR" }; @@ -157,8 +155,6 @@ export default function ConnectAccounts() { return ; case "brex": return ; - case "doorloop": - return ; case "xero": return ; case "gusto": @@ -239,13 +235,6 @@ export default function ConnectAccounts() { connected={connectedServices.includes("wavapps")} onConnect={() => handleConnect("wavapps")} /> - handleConnect("doorloop")} - /> {/* Payments & Payroll Integrations */} diff --git a/client/src/pages/Connections.tsx b/client/src/pages/Connections.tsx index 16d7273..0484a24 100755 --- a/client/src/pages/Connections.tsx +++ b/client/src/pages/Connections.tsx @@ -42,15 +42,6 @@ const integrationConfigs = [ requiresApproval: false, features: ['Payment processing', 'Subscription management', 'Customer portal'], }, - { - type: 'doorloop', - name: 'DoorLoop', - description: 'Property management integration for rent collection and maintenance tracking', - icon: '🏠', - docsUrl: 'https://www.doorloop.com', - requiresApproval: false, - features: ['Rent roll', 'Maintenance requests', 'Lease management'], - }, ]; export default function Connections() { diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx new file mode 100644 index 0000000..76291fe --- /dev/null +++ b/client/src/pages/Dashboard.tsx @@ -0,0 +1,342 @@ +import { useState } from 'react'; +import { Link } from 'wouter'; +import { + DollarSign, TrendingUp, TrendingDown, BarChart3, Users, + ArrowUpRight, ArrowDownRight, Send, Loader2, + Plug, ChevronRight, Sparkles, Building2 +} from 'lucide-react'; +import { usePortfolioSummary } from '@/hooks/use-property'; +import { useTenantId, useTenant } from '@/contexts/TenantContext'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { formatCurrency } from '@/lib/utils'; +import type { Transaction } from '@shared/schema'; + +/* ─── KPI Metric Card ─── */ +function MetricCard({ + label, value, sub, icon: Icon, delta, delay, +}: { + label: string; + value: string; + sub: string; + icon: React.ElementType; + delta?: { value: string; positive: boolean }; + delay: number; +}) { + return ( +
+
+ {label} +
+ +
+
+
{value}
+
+ {sub} + {delta && ( + + {delta.positive ? : } + {delta.value} + + )} +
+
+ ); +} + +/* ─── Transaction Row ─── */ +function TxnRow({ title, description, amount, date }: { + title: string; description?: string; amount: number; date?: string; +}) { + const positive = amount >= 0; + return ( +
+
+ {positive + ? + : + } +
+
+

{title}

+ {description &&

{description}

} +
+ {date && {date}} + + {positive ? '+' : ''}{formatCurrency(amount)} + +
+ ); +} + +/* ─── AI Chat Inline ─── */ +function AIQuickChat() { + const [input, setInput] = useState(''); + const [messages, setMessages] = useState>([ + { role: 'assistant', content: 'Ready. Ask about cash flow, optimization, or property performance.' }, + ]); + + const ask = useMutation({ + mutationFn: async (q: string) => { + const r = await fetch('/api/ai/property-advice', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: q }), + }); + if (!r.ok) return { content: 'Unable to reach AI advisor.' }; + return r.json() as Promise<{ content: string }>; + }, + onSuccess: (data) => setMessages(prev => [...prev, { role: 'assistant', content: data.content }]), + }); + + const send = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || ask.isPending) return; + setMessages(prev => [...prev, { role: 'user', content: input }]); + ask.mutate(input); + setInput(''); + }; + + return ( +
+ {/* Header */} +
+ + AI CFO + GPT-4o +
+ + {/* Messages */} +
+ {messages.map((m, i) => ( +
+
+ {m.content} +
+
+ ))} + {ask.isPending && ( +
+
+ +
+
+ )} +
+ + {/* Input */} +
+ setInput(e.target.value)} + placeholder="Ask about your finances..." + disabled={ask.isPending} + className="flex-1 bg-[hsl(var(--cf-raised))] text-sm text-[hsl(var(--cf-text))] placeholder:text-[hsl(var(--cf-text-muted))] rounded-md px-3 py-2 border border-[hsl(var(--cf-border-subtle))] focus:border-[hsl(var(--cf-lime)/0.4)] focus:outline-none transition-colors" + /> + +
+
+ ); +} + +/* ─── Integration Status Strip ─── */ +function IntegrationStrip() { + const { data } = useQuery>({ + queryKey: ['/api/integrations/status'], + staleTime: 60_000, + }); + + const services = [ + { key: 'mercury', label: 'Mercury', color: '--cf-cyan' }, + { key: 'wave', label: 'Wave', color: '--cf-violet' }, + { key: 'stripe', label: 'Stripe', color: '--cf-amber' }, + { key: 'openai', label: 'OpenAI', color: '--cf-lime' }, + ]; + + return ( +
+
+
+ + Integrations +
+ + + Manage + + +
+
+ {services.map(s => { + const configured = data?.[s.key]?.configured ?? false; + return ( +
+ + {s.label} + + {configured ? 'live' : 'off'} + +
+ ); + })} +
+
+ ); +} + +/* ─── Main Dashboard ─── */ +export default function Dashboard() { + const tenantId = useTenantId(); + const { currentTenant } = useTenant(); + const { data: portfolio, isLoading: portfolioLoading } = usePortfolioSummary(); + const { data: transactions = [] } = useQuery({ + queryKey: ['/api/transactions', tenantId, { limit: 6 }], + enabled: !!tenantId, + }); + + if (!tenantId) { + return ( +
+

Select a tenant to view the dashboard.

+
+ ); + } + + const recent = transactions.slice(0, 6); + + return ( +
+ {/* Page Header */} +
+

+ Financial Overview +

+

+ {currentTenant?.name || 'All entities'} — {new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} +

+
+ + {/* KPI Strip */} +
+ + + + +
+ + {/* Two Column: Activity + AI */} +
+ {/* Left: Recent Activity (3 cols) */} +
+ {/* Recent Transactions */} +
+
+ Recent Activity + + + View all + + +
+ {recent.length === 0 ? ( +
+ No recent transactions +
+ ) : ( +
+ {recent.map((tx, i) => ( + + ))} +
+ )} +
+ + {/* Quick Property Summary */} + {portfolio && portfolio.properties.length > 0 && ( +
+
+ Properties + + + Portfolio + + +
+
+ {portfolio.properties.slice(0, 4).map(p => ( + +
+
+ +
+
+

{p.name}

+

{p.city}, {p.state}

+
+
+

{formatCurrency(p.currentValue)}

+

{p.capRate.toFixed(1)}% cap

+
+
+ + ))} +
+
+ )} +
+ + {/* Right: AI + Integrations (2 cols) */} +
+ + +
+
+
+ ); +} diff --git a/server/integrations/doorloopClient.ts b/server/integrations/doorloopClient.ts deleted file mode 100755 index 2733d06..0000000 --- a/server/integrations/doorloopClient.ts +++ /dev/null @@ -1,54 +0,0 @@ -// server/integrations/doorloopClient.ts - -const BASE = "https://app.doorloop.com/api"; - -// Generic fetch helper -export async function dlFetch(path: string, apiKey: string) { - const res = await fetch(`${BASE}${path}`, { - headers: { - Authorization: `bearer ${apiKey}`, - accept: "application/json", - }, - }); - - const text = await res.text(); - const trimmed = text.trim(); - - // Error responses - if (!res.ok) { - throw new Error(`DoorLoop error ${res.status}: ${trimmed}`); - } - - // If HTML is returned instead of JSON - if (trimmed.startsWith("<")) { - // Payments endpoint frequently returns HTML for non-premium accounts - if (path.startsWith("/payments")) { - return { data: [], html: trimmed }; - } - throw new Error("DoorLoop returned HTML instead of JSON."); - } - - // Parse JSON content - try { - return JSON.parse(trimmed); - } catch { - throw new Error(`Invalid JSON from DoorLoop: ${trimmed.slice(0, 200)}`); - } -} - -// ----------------------------- -// API Wrapper Methods -// ----------------------------- - -export async function listProperties(apiKey: string) { - return dlFetch("/properties?limit=200&offset=0", apiKey); -} - -export async function listLeases(apiKey: string) { - return dlFetch("/leases?limit=200&offset=0", apiKey); -} - -export async function listPayments(apiKey: string) { - // Payments endpoint: HTML fallback is handled by dlFetch - return dlFetch("/payments?limit=200&offset=0", apiKey); -} diff --git a/server/lib/bookkeeping-workflows.ts b/server/lib/bookkeeping-workflows.ts index 9571765..1d1a3c3 100644 --- a/server/lib/bookkeeping-workflows.ts +++ b/server/lib/bookkeeping-workflows.ts @@ -6,7 +6,6 @@ import { WaveBookkeepingClient } from './wave-bookkeeping'; import { ChittyRentalClient } from './chittyrental-integration'; -import { DoorLoopClient } from './doorloop-integration'; import { StripeConnectClient } from './stripe-connect'; import { storage } from '../storage'; import { logToChronicle } from './chittychronicle-logging'; @@ -34,7 +33,6 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ synced: { wave: number; rental: number; - doorloop: number; stripe: number; }; categorized: number; @@ -43,7 +41,7 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ console.log(`Running daily bookkeeping for tenant ${tenantId}`); const result = { - synced: { wave: 0, rental: 0, doorloop: 0, stripe: 0 }, + synced: { wave: 0, rental: 0, stripe: 0 }, categorized: 0, anomalies: 0, }; @@ -69,35 +67,7 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ } } - // 2. Sync DoorLoop data (real property management system) - const doorloopIntegrations = await storage.listIntegrationsByService('doorloop'); - for (const integration of doorloopIntegrations) { - if (integration.tenantId === tenantId && integration.connected) { - try { - const credentials = integration.credentials as any; - const doorloopClient = new DoorLoopClient(credentials.api_key); - - // Get all DoorLoop properties - const doorloopProperties = await doorloopClient.getProperties(); - - // Sync each property - for (const dlProperty of doorloopProperties) { - const syncResult = await doorloopClient.syncProperty( - dlProperty.id, - tenantId, - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() // Last 7 days - ); - - result.synced.doorloop += syncResult.rentPayments + syncResult.expenses; - } - - console.log(`✅ Synced ${doorloopProperties.length} DoorLoop properties`); - } catch (error) { - console.error('DoorLoop sync error:', error); - // Continue with other integrations even if DoorLoop fails - } - } - } + // 2. (DoorLoop removed — property management now handled via TurboTenant CSV import) // 3. Sync ChittyRental data (if using ChittyOS rental service) const properties = await storage.getProperties?.(tenantId); diff --git a/server/lib/chargeAutomation.ts b/server/lib/chargeAutomation.ts index 8dc129c..99e5a0f 100755 --- a/server/lib/chargeAutomation.ts +++ b/server/lib/chargeAutomation.ts @@ -42,10 +42,6 @@ export async function getRecurringCharges(userId: number | string): Promise { - console.warn('DoorLoop recurring charge detection not yet implemented'); - return []; -} - // TODO: Wire to Stripe API (real integration exists in stripe.ts) async function fetchStripeCharges(_integration: Integration): Promise { console.warn('Stripe recurring charge detection not yet implemented'); diff --git a/server/lib/doorloop-integration.ts b/server/lib/doorloop-integration.ts deleted file mode 100644 index 8bf9c99..0000000 --- a/server/lib/doorloop-integration.ts +++ /dev/null @@ -1,545 +0,0 @@ -// @ts-nocheck - TODO: Add proper types -/** - * Enhanced DoorLoop Property Management Integration - * Real-time property management, rent collection, and financial tracking - */ - -import { fetchWithRetry, IntegrationError } from './error-handling'; -import { storage } from '../storage'; -import { logToChronicle } from './chittychronicle-logging'; -import { validateTransaction } from './chittyschema-validation'; - -const DOORLOOP_BASE_URL = 'https://app.doorloop.com/api'; -const DOORLOOP_API_KEY = process.env.DOORLOOP_API_KEY; - -export interface DoorLoopProperty { - id: number; - name: string; - address: { - line1: string; - line2?: string; - city: string; - state: string; - zip: string; - country: string; - full?: string; - }; - type: string; - units: number; - status: string; - portfolio?: { - id: number; - name: string; - }; - createdAt: string; - updatedAt: string; -} - -export interface DoorLoopUnit { - id: number; - propertyId: number; - name: string; - bedrooms: number; - bathrooms: number; - sqft?: number; - marketRent?: number; - status: string; - createdAt: string; -} - -export interface DoorLoopLease { - id: number; - propertyId: number; - unitId?: number; - tenant: { - id: number; - name: string; - email?: string; - phone?: string; - }; - startDate: string; - endDate: string; - monthlyRent: number; - securityDeposit: number; - status: 'active' | 'past' | 'future' | 'terminated'; - leaseType: string; - createdAt: string; - updatedAt: string; -} - -export interface DoorLoopPayment { - id: number; - leaseId: number; - tenantId: number; - amount: number; - date: string; - status: 'pending' | 'cleared' | 'void' | 'bounced'; - paymentMethod: string; - memo?: string; - reference?: string; - createdAt: string; -} - -export interface DoorLoopExpense { - id: number; - propertyId: number; - amount: number; - date: string; - category: string; - vendor?: string; - description: string; - status: string; - createdAt: string; -} - -export interface DoorLoopMaintenanceRequest { - id: number; - propertyId: number; - unitId?: number; - title: string; - description: string; - priority: 'low' | 'medium' | 'high' | 'emergency'; - status: 'open' | 'in_progress' | 'completed' | 'cancelled'; - assignedTo?: string; - createdAt: string; - updatedAt: string; -} - -/** - * DoorLoop API Client - */ -export class DoorLoopClient { - private apiKey: string; - private baseUrl: string; - - constructor(apiKey?: string, baseUrl: string = DOORLOOP_BASE_URL) { - this.apiKey = apiKey || DOORLOOP_API_KEY || ''; - this.baseUrl = baseUrl; - - if (!this.apiKey) { - throw new Error('DoorLoop API key is required. Set DOORLOOP_API_KEY environment variable.'); - } - } - - private async request(endpoint: string, options?: RequestInit): Promise { - const url = `${this.baseUrl}${endpoint}`; - - const response = await fetchWithRetry( - url, - { - ...options, - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - ...options?.headers, - }, - }, - { - maxRetries: 2, - baseDelay: 1000, - } - ); - - const text = await response.text(); - const trimmed = text.trim(); - - // Error responses - if (!response.ok) { - throw new IntegrationError( - `DoorLoop API error ${response.status}: ${trimmed}`, - 'doorloop', - response.status >= 500 - ); - } - - // If HTML is returned instead of JSON (non-premium account) - if (trimmed.startsWith('<')) { - // Payments endpoint frequently returns HTML for non-premium accounts - if (endpoint.startsWith('/payments')) { - console.warn('⚠️ DoorLoop payments endpoint returned HTML (likely requires premium account)'); - return { data: [], html: trimmed, isPremiumRequired: true } as unknown as T; - } - throw new IntegrationError( - 'DoorLoop returned HTML instead of JSON. This may require a premium account.', - 'doorloop', - false - ); - } - - // Parse JSON content - try { - return JSON.parse(trimmed); - } catch (error) { - throw new IntegrationError( - `Invalid JSON from DoorLoop: ${trimmed.slice(0, 200)}`, - 'doorloop', - false - ); - } - } - - /** - * Get all properties - */ - async getProperties(limit: number = 200, offset: number = 0): Promise { - const response = await this.request<{ data: DoorLoopProperty[] }>( - `/properties?limit=${limit}&offset=${offset}` - ); - return response.data || []; - } - - /** - * Get property by ID - */ - async getProperty(propertyId: number): Promise { - const response = await this.request(`/properties/${propertyId}`); - return response; - } - - /** - * Get units for a property - */ - async getUnits(propertyId: number): Promise { - const response = await this.request<{ data: DoorLoopUnit[] }>( - `/properties/${propertyId}/units` - ); - return response.data || []; - } - - /** - * Get all leases - */ - async getLeases(limit: number = 200, offset: number = 0): Promise { - const response = await this.request<{ data: DoorLoopLease[] }>( - `/leases?limit=${limit}&offset=${offset}` - ); - return response.data || []; - } - - /** - * Get leases for a specific property - */ - async getPropertyLeases(propertyId: number): Promise { - const allLeases = await this.getLeases(); - return allLeases.filter(lease => lease.propertyId === propertyId); - } - - /** - * Get lease by ID - */ - async getLease(leaseId: number): Promise { - const response = await this.request(`/leases/${leaseId}`); - return response; - } - - /** - * Get payments (may require premium account) - */ - async getPayments(limit: number = 200, offset: number = 0): Promise { - const response = await this.request<{ data: DoorLoopPayment[]; isPremiumRequired?: boolean }>( - `/payments?limit=${limit}&offset=${offset}` - ); - - if (response.isPremiumRequired) { - console.warn('⚠️ DoorLoop payments require premium account - returning empty array'); - return []; - } - - return response.data || []; - } - - /** - * Get payments for a specific lease - */ - async getLeasePayments(leaseId: number): Promise { - try { - const allPayments = await this.getPayments(); - return allPayments.filter(payment => payment.leaseId === leaseId); - } catch (error) { - console.warn(`Failed to fetch payments for lease ${leaseId}:`, error); - return []; - } - } - - /** - * Get expenses - */ - async getExpenses(limit: number = 200, offset: number = 0): Promise { - const response = await this.request<{ data: DoorLoopExpense[] }>( - `/expenses?limit=${limit}&offset=${offset}` - ); - return response.data || []; - } - - /** - * Get maintenance requests - */ - async getMaintenanceRequests(propertyId?: number): Promise { - const endpoint = propertyId - ? `/maintenance?propertyId=${propertyId}` - : '/maintenance'; - - const response = await this.request<{ data: DoorLoopMaintenanceRequest[] }>(endpoint); - return response.data || []; - } - - /** - * Find property by address - */ - async findPropertyByAddress(searchAddress: string): Promise { - const properties = await this.getProperties(); - return properties.find(p => - p.address?.line1?.toLowerCase().includes(searchAddress.toLowerCase()) || - p.address?.full?.toLowerCase().includes(searchAddress.toLowerCase()) || - p.name?.toLowerCase().includes(searchAddress.toLowerCase()) - ); - } - - /** - * Sync rent payments to ChittyFinance - */ - async syncRentPayments(propertyId: number, tenantId: string, startDate?: string): Promise<{ - synced: number; - errors: string[]; - }> { - let synced = 0; - const errors: string[] = []; - - try { - const leases = await this.getPropertyLeases(propertyId); - - for (const lease of leases) { - try { - const payments = await this.getLeasePayments(lease.id); - - for (const payment of payments) { - // Filter by start date if provided - if (startDate && new Date(payment.date) < new Date(startDate)) { - continue; - } - - // Only sync cleared payments - if (payment.status !== 'cleared') { - continue; - } - - // Check if already synced - const existing = await storage.getTransactions(tenantId); - const alreadySynced = existing.some(t => t.externalId === `doorloop-payment-${payment.id}`); - - if (!alreadySynced) { - const transactionData = { - tenantId, - accountId: 'doorloop-rent-income', // Would map to correct account - amount: payment.amount.toString(), - type: 'income', - description: `Rent payment - ${lease.tenant.name}`, - date: new Date(payment.date), - category: 'rent_income', - payee: lease.tenant.name, - externalId: `doorloop-payment-${payment.id}`, - propertyId: propertyId.toString(), - reconciled: true, - metadata: { - source: 'doorloop', - leaseId: lease.id, - paymentMethod: payment.paymentMethod, - reference: payment.reference, - }, - }; - - // Validate with ChittySchema - try { - const validation = await validateTransaction(transactionData); - if (!validation.valid) { - errors.push(`Validation failed for payment ${payment.id}: ${validation.errors?.map(e => e.message).join(', ')}`); - continue; - } - } catch (error) { - // Log validation error but continue (schema service may be unavailable) - console.warn(`ChittySchema validation unavailable for payment ${payment.id}:`, error); - } - - await storage.createTransaction(transactionData); - synced++; - } - } - } catch (error) { - errors.push(`Failed to sync lease ${lease.id}: ${error}`); - } - } - - // Log sync - await logToChronicle({ - eventType: 'integration_sync', - entityId: tenantId, - entityType: 'tenant', - action: 'doorloop_rent_sync', - metadata: { - propertyId, - synced, - errors: errors.length, - timestamp: new Date().toISOString(), - }, - }); - - } catch (error) { - errors.push(`Sync failed: ${error}`); - } - - return { synced, errors }; - } - - /** - * Sync property expenses to ChittyFinance - */ - async syncExpenses(propertyId: number, tenantId: string, startDate?: string): Promise<{ - synced: number; - errors: string[]; - }> { - let synced = 0; - const errors: string[] = []; - - try { - const allExpenses = await this.getExpenses(); - const propertyExpenses = allExpenses.filter(e => e.propertyId === propertyId); - - for (const expense of propertyExpenses) { - // Filter by start date if provided - if (startDate && new Date(expense.date) < new Date(startDate)) { - continue; - } - - try { - // Check if already synced - const existing = await storage.getTransactions(tenantId); - const alreadySynced = existing.some(t => t.externalId === `doorloop-expense-${expense.id}`); - - if (!alreadySynced) { - const transactionData = { - tenantId, - accountId: 'doorloop-expense-account', - amount: (-expense.amount).toString(), - type: 'expense', - description: expense.description, - date: new Date(expense.date), - category: expense.category || 'other_expense', - payee: expense.vendor, - externalId: `doorloop-expense-${expense.id}`, - propertyId: propertyId.toString(), - reconciled: true, - metadata: { - source: 'doorloop', - expenseStatus: expense.status, - }, - }; - - // Validate with ChittySchema - try { - const validation = await validateTransaction(transactionData); - if (!validation.valid) { - errors.push(`Validation failed for expense ${expense.id}: ${validation.errors?.map(e => e.message).join(', ')}`); - continue; - } - } catch (error) { - // Log validation error but continue (schema service may be unavailable) - console.warn(`ChittySchema validation unavailable for expense ${expense.id}:`, error); - } - - await storage.createTransaction(transactionData); - synced++; - } - } catch (error) { - errors.push(`Failed to sync expense ${expense.id}: ${error}`); - } - } - - // Log sync - await logToChronicle({ - eventType: 'integration_sync', - entityId: tenantId, - entityType: 'tenant', - action: 'doorloop_expense_sync', - metadata: { - propertyId, - synced, - errors: errors.length, - timestamp: new Date().toISOString(), - }, - }); - - } catch (error) { - errors.push(`Expense sync failed: ${error}`); - } - - return { synced, errors }; - } - - /** - * Full sync for a property - */ - async syncProperty(propertyId: number, tenantId: string, startDate?: string): Promise<{ - rentPayments: number; - expenses: number; - errors: string[]; - }> { - const [rentResult, expenseResult] = await Promise.all([ - this.syncRentPayments(propertyId, tenantId, startDate), - this.syncExpenses(propertyId, tenantId, startDate), - ]); - - return { - rentPayments: rentResult.synced, - expenses: expenseResult.synced, - errors: [...rentResult.errors, ...expenseResult.errors], - }; - } - - /** - * Debug: Test API connection - */ - async testConnection(): Promise<{ - connected: boolean; - properties: number; - leases: number; - paymentsAvailable: boolean; - error?: string; - }> { - try { - const properties = await this.getProperties(1, 0); - const leases = await this.getLeases(1, 0); - - let paymentsAvailable = false; - try { - const payments = await this.getPayments(1, 0); - paymentsAvailable = payments.length >= 0; - } catch (error) { - // Payments may not be available on free tier - paymentsAvailable = false; - } - - return { - connected: true, - properties: properties.length, - leases: leases.length, - paymentsAvailable, - }; - } catch (error) { - return { - connected: false, - properties: 0, - leases: 0, - paymentsAvailable: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - } -} - -/** - * Create DoorLoop client instance - */ -export function createDoorLoopClient(apiKey?: string): DoorLoopClient { - return new DoorLoopClient(apiKey); -} diff --git a/server/lib/doorloop-sync.ts b/server/lib/doorloop-sync.ts deleted file mode 100755 index 4e9aa24..0000000 --- a/server/lib/doorloop-sync.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DoorLoop } from "./doorloop.js"; -import { storage } from "../storage.js"; - -export async function syncDoorLoopFull(userId: string, tenantId?: string) { - const [props, leases, payments] = await Promise.all([ - DoorLoop.properties() as Promise<{ data?: any[] }>, - DoorLoop.leases() as Promise<{ data?: any[] }>, - DoorLoop.payments() as Promise<{ data?: any[] }>, - ]); - - for (const lease of leases?.data ?? []) { - const ledger = await DoorLoop.ledger(lease.id) as { data?: any[] }; - - for (const e of ledger?.data ?? []) { - const amount = e.amount ?? e.debit ?? e.credit ?? 0; - - await (storage as any).createTransaction({ - userId, - tenantId, - source: "doorloop", - externalId: e.id?.toString?.() ?? undefined, - amount, - date: e.date ?? e.createdAt ?? null, - type: e.type ?? "doorloop_ledger", - description: e.description ?? e.memo ?? null, - }); - } - } - - return { - properties: props?.data ?? [], - leases: leases?.data ?? [], - payments: payments?.data ?? [], - }; -} diff --git a/server/lib/doorloop.ts b/server/lib/doorloop.ts deleted file mode 100755 index 26e7d13..0000000 --- a/server/lib/doorloop.ts +++ /dev/null @@ -1,38 +0,0 @@ -const BASE = process.env.DOORLOOP_BASE_URL || "https://api.doorloop.com/v1"; - -function auth() { - const key = process.env.DOORLOOP_API_KEY; - if (!key) { - throw new Error("DOORLOOP_API_KEY not set"); - } - return { - headers: { - Authorization: `Bearer ${key}`, - "Content-Type": "application/json", - }, - }; -} - -async function get(path: string) { - const res = await fetch(`${BASE}${path}`, auth() as any); - if (!res.ok) { - const text = await res.text(); - throw new Error(`DoorLoop GET ${path} → ${res.status} ${text}`); - } - return res.json(); -} - -export const DoorLoop = { - properties() { - return get("/properties"); - }, - leases() { - return get("/leases"); - }, - payments() { - return get("/payments"); - }, - ledger(leaseId: string) { - return get(`/leases/${leaseId}/ledger`); - }, -}; diff --git a/server/lib/doorloop/index.ts b/server/lib/doorloop/index.ts deleted file mode 100755 index ff7955c..0000000 --- a/server/lib/doorloop/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - listProperties, - listLeases, - listPayments -} from "../../integrations/doorloopClient.js"; - -const key = process.env.DOORLOOP_API_KEY!; - -export async function dlGetProperties() { - return listProperties(key); -} -export async function dlGetLeases() { - return listLeases(key); -} -export async function dlGetPayments() { - return listPayments(key); -} diff --git a/server/lib/doorloop/normalize.ts b/server/lib/doorloop/normalize.ts deleted file mode 100755 index dd1da8b..0000000 --- a/server/lib/doorloop/normalize.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function normalizeDoorloopTransaction(p: any) { - return { - // ChittyFinance transaction fields - amount: p.amount || p.total || 0, - date: p.date || p.createdAt, - description: p.memo || p.type || "DoorLoop Transaction", - category: "rent", - source: "doorloop", - }; -} diff --git a/server/lib/doorloop/sync.ts b/server/lib/doorloop/sync.ts deleted file mode 100644 index 4083142..0000000 --- a/server/lib/doorloop/sync.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-nocheck - TODO: Add proper types -import { storage } from "../../storage.js"; -import { listProperties, listLeases, listPayments } from "../../integrations/doorloopClient.js"; -import { normalizeDoorloopTransaction } from "./normalize.js"; - -export async function syncDoorloop() { - const apiKey = process.env.DOORLOOP_API_KEY!; - - // Fetch all DoorLoop data - const [properties, leases, payments] = await Promise.all([ - listProperties(apiKey), - listLeases(apiKey), - listPayments(apiKey), - ]); - - // Normalize payments → ChittyFinance transactions - const normalized = []; - for (const p of payments.data || []) { - normalized.push(normalizeDoorloopTransaction(p)); - } - - // Insert into ChittyFinance DB - for (const tx of normalized) { - await storage.createTransaction({ - userId: 1, // demo user - ...tx, - }); - } - - // Log sync event - const payload = { properties, leases, payments }; - const eventId = `doorloop-sync-${new Date().toISOString()}`; - - await storage.recordWebhookEvent({ - source: "doorloop", - eventId, - payload: payload as any, - }); - - return { - eventId, - counts: { - properties: properties?.data?.length ?? 0, - leases: leases?.data?.length ?? 0, - payments: payments?.data?.length ?? 0, - normalizedTransactions: normalized.length, - }, - }; -} diff --git a/server/lib/error-handling.ts b/server/lib/error-handling.ts index 7a6028d..5e3da02 100644 --- a/server/lib/error-handling.ts +++ b/server/lib/error-handling.ts @@ -347,6 +347,5 @@ export const circuitBreakers = { mercury: new CircuitBreaker(5, 60000), wave: new CircuitBreaker(5, 60000), stripe: new CircuitBreaker(5, 60000), - doorloop: new CircuitBreaker(5, 60000), chittyConnect: new CircuitBreaker(3, 30000), }; diff --git a/server/lib/financialServices.ts b/server/lib/financialServices.ts index da0ac8d..58e3d5d 100755 --- a/server/lib/financialServices.ts +++ b/server/lib/financialServices.ts @@ -136,12 +136,6 @@ export async function fetchWavAppsData(integration: Integration): Promise> { - console.warn('DoorLoop integration not yet implemented — returning empty data'); - return {}; -} - // TODO: Wire to Stripe API for financial data (real integration exists in stripe.ts for payments) export async function fetchStripeData(_integration: Integration): Promise> { console.warn('Stripe financial data fetch not yet implemented — returning empty data'); @@ -203,9 +197,6 @@ export async function getAggregatedFinancialData(integrations: Integration[]): P case 'wavapps': serviceData = await fetchWavAppsData(integration); break; - case 'doorloop': - serviceData = await fetchDoorLoopData(integration); - break; case 'stripe': serviceData = await fetchStripeData(integration); break; diff --git a/server/lib/universalConnector.ts b/server/lib/universalConnector.ts index 91a84b5..f4583af 100755 --- a/server/lib/universalConnector.ts +++ b/server/lib/universalConnector.ts @@ -1,9 +1,8 @@ import { Integration } from "@shared/schema"; import { FinancialData } from "./financialServices"; -import { +import { fetchMercuryBankData, fetchWavAppsData, - fetchDoorLoopData, fetchStripeData, fetchQuickBooksData, fetchXeroData, @@ -178,7 +177,6 @@ export async function transformToUniversalFormat( function getSourceFromTransactionId(id: string): string { if (id.startsWith('merc-')) return 'mercury_bank'; if (id.startsWith('wavapps-')) return 'wavapps'; - if (id.startsWith('doorloop-')) return 'doorloop'; if (id.startsWith('stripe-')) return 'stripe'; if (id.startsWith('qb-')) return 'quickbooks'; if (id.startsWith('xero-')) return 'xero'; @@ -193,7 +191,6 @@ function getSourceFromTransactionId(id: string): string { function getSourceFromChargeId(id: string): string { if (id.startsWith('merc-charge-')) return 'mercury_bank'; if (id.startsWith('wavapps-charge-')) return 'wavapps'; - if (id.startsWith('doorloop-charge-')) return 'doorloop'; if (id.startsWith('stripe-charge-')) return 'stripe'; if (id.startsWith('qb-charge-')) return 'quickbooks'; if (id.startsWith('xero-charge-')) return 'xero'; diff --git a/server/routes.ts b/server/routes.ts index ea2eb09..6a078ab 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -915,7 +915,6 @@ export async function registerRoutes(app: Express): Promise { switch (platformId) { case "mercury_bank": case "wavapps": - case "doorloop": case "stripe": case "quickbooks": case "xero": @@ -946,7 +945,7 @@ export async function registerRoutes(app: Express): Promise { let recurringCharges; // Only test recurring charges on platforms that support it - if (["mercury_bank", "wavapps", "doorloop", "stripe", "quickbooks", "xero", "brex"].includes(platformId)) { + if (["mercury_bank", "wavapps", "stripe", "quickbooks", "xero", "brex"].includes(platformId)) { recurringCharges = await getRecurringCharges(user.id); results.tests.push({ diff --git a/server/routes/doorloop.ts b/server/routes/doorloop.ts deleted file mode 100755 index 5b80902..0000000 --- a/server/routes/doorloop.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck - This file uses Fastify but the project uses Express. Needs migration. -import type { FastifyInstance } from "fastify"; -import { listProperties, listLeases, listPayments } from "../integrations/doorloopClient.js"; - -export default async function doorloopRoutes(app: FastifyInstance) { - app.get("/doorloop/properties", async (req: any, res: any) => { - const apiKey = process.env.DOORLOOP_API_KEY; - const data = await listProperties(apiKey); - return res.send(data); - }); - - app.get("/doorloop/leases", async (req: any, res: any) => { - const apiKey = process.env.DOORLOOP_API_KEY; - const data = await listLeases(apiKey); - return res.send(data); - }); - - app.get("/doorloop/payments", async (req: any, res: any) => { - const apiKey = process.env.DOORLOOP_API_KEY; - const data = await listPayments(apiKey); - return res.send(data); - }); -}