Skip to content

feat: TurboTenant deposits + rent roll import#106

Merged
chitcommit merged 1 commit intomainfrom
feat/turbotenant-deposits-import
Apr 24, 2026
Merged

feat: TurboTenant deposits + rent roll import#106
chitcommit merged 1 commit intomainfrom
feat/turbotenant-deposits-import

Conversation

@chitcommit
Copy link
Copy Markdown
Contributor

@chitcommit chitcommit commented Apr 24, 2026

Summary

  • POST /api/import/turbotenant-deposits — ingest TurboTenant deposit/payment history
    • Dedup via Payment ID (tt-deposit:<id>)
    • Auto-resolve Lease Address → tenant, Bank Account → Mercury account
    • Classify rental income (4010) vs fee income (4020)
    • 70 rows from production export tested
  • POST /api/import/turbotenant-rentroll — ingest rent roll snapshot
    • Match properties by address pattern
    • Return structured rent roll (no tx creation, read-only snapshot)

Test plan

  • Type-check passes (npx tsc --noEmit)
  • Deploy to Workers and ingest deposits CSV from Google Drive
  • Verify 70 transactions created with correct tenant/account/COA mapping
  • Verify rent roll returns 4 matched units

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • TurboTenant deposits import functionality
    • TurboTenant rent roll import functionality

- POST /api/import/turbotenant-deposits — ingests payment history CSV
  with auto-resolution of property→tenant and bank→account mappings.
  Dedup via TurboTenant Payment ID. Classifies as rental income (4010)
  or fee income (4020) based on bank account pattern.

- POST /api/import/turbotenant-rentroll — ingests rent roll snapshot CSV.
  Matches properties by address, returns structured rent roll with
  unpaid/past due amounts. Read-only (no transaction creation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 12:10
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
chittyfinance 68054a1 Apr 24 2026, 12:10 PM

@chitcommit chitcommit enabled auto-merge (squash) April 24, 2026 12:10
@github-actions
Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request adds two new CSV import routes for TurboTenant data: a deposits importer that parses transaction records with tenant/account resolution and deduplication, and a rent roll importer that processes property occupancy snapshots with pattern-based tenant matching.

Changes

Cohort / File(s) Summary
TurboTenant Import Routes
server/routes/import.ts
Adds two POST endpoints: /api/import/turbotenant-deposits handles CSV deposit parsing with tenant resolution via bank-account mapping or address fallback, account matching via name patterns, transaction deduplication by external ID, and creates categorized income transactions; /api/import/turbotenant-rentroll parses rent-roll snapshots with hardcoded property-to-tenant mapping, validates required columns, aggregates monetary totals, and returns matched/unmatched row counts with parsed data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hop into deposits, parse them right,
Rent rolls bundled, shiny bright,
TurboTenant flows through with care,
Mapping tenants everywhere!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: introducing two new import endpoints for TurboTenant deposits and rent roll data.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/turbotenant-deposits-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chitcommit chitcommit merged commit fdd22bd into main Apr 24, 2026
11 of 15 checks passed
@chitcommit chitcommit deleted the feat/turbotenant-deposits-import branch April 24, 2026 12:11
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68054a189b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/routes/import.ts
Comment on lines +1217 to +1219
if (!tenantId) {
// Default to ARIBIA MGMT for unresolved
tenantId = slugToTenantId['aribia-mgmt'] || Object.values(slugToTenantId)[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict deposit imports to the caller tenant

/api/import/turbotenant-deposits is mounted under the tenant-protected API, but this handler chooses tenantId from CSV-derived mappings (and defaults to aribia-mgmt) instead of the authenticated c.get('tenantId'). In the current app wiring, any user who can pass tenant middleware for one tenant can call this route and create transactions in other active tenants, which breaks tenant isolation and is a cross-tenant write vulnerability.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds TurboTenant-specific import endpoints under the existing /api/import route group to support ingesting deposit/payment history into transactions and parsing rent roll CSV snapshots for reporting.

Changes:

  • Adds POST /api/import/turbotenant-deposits to parse TurboTenant deposit CSVs, dedupe by TurboTenant Payment ID, and create income transactions with COA suggestions.
  • Adds POST /api/import/turbotenant-rentroll to parse rent roll CSVs and return a structured snapshot with basic address-pattern matching.
  • Adds audit logging for both imports via ledgerLog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/routes/import.ts
Comment on lines +1218 to +1219
// Default to ARIBIA MGMT for unresolved
tenantId = slugToTenantId['aribia-mgmt'] || Object.values(slugToTenantId)[0];
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The unresolved-tenant fallback Object.values(slugToTenantId)[0] can route imports into an arbitrary tenant (and can be undefined if no tenants exist). That’s risky for data integrity. Prefer returning a 400 with a clear error (or skipping the row with an error counter) when neither bank account nor address can resolve a tenant.

Suggested change
// Default to ARIBIA MGMT for unresolved
tenantId = slugToTenantId['aribia-mgmt'] || Object.values(slugToTenantId)[0];
skipped++;
continue;

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
Comment on lines +1159 to +1163
function resolveBankAccount(bankAcctStr: string): { tenantSlug: string; namePattern: string } | null {
const lower = bankAcctStr.toLowerCase();
// Try most specific match first (longest key)
const sorted = Object.entries(BANK_ACCOUNT_MAP).sort((a, b) => b[0].length - a[0].length);
for (const [pattern, mapping] of sorted) {
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

resolveBankAccount sorts BANK_ACCOUNT_MAP entries on every call, which makes per-row processing O(n log n) unnecessarily. Precompute the sorted entries once (outside the function) and iterate that array in resolveBankAccount.

Suggested change
function resolveBankAccount(bankAcctStr: string): { tenantSlug: string; namePattern: string } | null {
const lower = bankAcctStr.toLowerCase();
// Try most specific match first (longest key)
const sorted = Object.entries(BANK_ACCOUNT_MAP).sort((a, b) => b[0].length - a[0].length);
for (const [pattern, mapping] of sorted) {
// Try most specific match first (longest key)
const sortedBankAccountEntries = Object.entries(BANK_ACCOUNT_MAP).sort(
(a, b) => b[0].length - a[0].length,
);
function resolveBankAccount(bankAcctStr: string): { tenantSlug: string; namePattern: string } | null {
const lower = bankAcctStr.toLowerCase();
for (const [pattern, mapping] of sortedBankAccountEntries) {

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
// POST /api/import/turbotenant-rentroll — import TurboTenant rent roll snapshot
// Accepts raw CSV body with columns: Property, Unit, Tenants, Lease Start,
// Lease End, Security Deposit, Rent Amount, Total Unpaid, Total Past Due
// Upserts leases on matching properties. Does not create transactions.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The comment says this route “Upserts leases on matching properties”, but the implementation only parses CSV and returns a read-only snapshot (no storage writes). Update the comment to match current behavior (or implement the upsert if that’s intended).

Suggested change
// Upserts leases on matching properties. Does not create transactions.
// Parses the CSV and returns a read-only snapshot. Does not upsert leases or create transactions.

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
return c.json({ error: 'CSV must have header + data rows' }, 400);
}

const cols = parseCSVLine(lines[startLine]);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

BOM handling here is incomplete: when the first line contains \uFEFF but is actually the header row (no “Pulled on…” preamble), cleaned is computed but never used, so parseCSVLine(lines[startLine]) may still see the BOM and colIdx(...) can fail. Strip a leading BOM from the actual header line unconditionally before parsing.

Suggested change
const cols = parseCSVLine(lines[startLine]);
const headerLine = (lines[startLine] ?? '').replace(/^\ufeff/, '');
const cols = parseCSVLine(headerLine);

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
Comment on lines +1349 to +1352
const allTenants = await storage.getTenants();
const slugToTenantId: Record<string, string> = {};
for (const t of allTenants) slugToTenantId[t.slug] = t.id;

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

allTenants/slugToTenantId are computed but never used in this route, which adds unnecessary DB work and makes the code harder to follow. Either remove this block or use it to return richer match info (e.g., tenantId/propertyId) as part of the response.

Suggested change
const allTenants = await storage.getTenants();
const slugToTenantId: Record<string, string> = {};
for (const t of allTenants) slugToTenantId[t.slug] = t.id;

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
Comment on lines +1137 to +1141
// Load tenants and accounts
const allTenants = await storage.getTenants();
const slugToTenantId: Record<string, string> = {};
for (const t of allTenants) slugToTenantId[t.slug] = t.id;

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This endpoint ignores the caller-scoped tenantId from tenantMiddleware and instead loads all active tenants (storage.getTenants()) and writes transactions into whatever tenantId is inferred from the CSV. That bypasses the membership check in tenantMiddleware and allows a caller with access to a single tenant to create transactions in other tenants. Restrict imports to c.get('tenantId') (and only resolve accounts/properties within that tenant), or explicitly verify the caller is a member of any resolved tenantId (e.g., via storage.getUserTenants(c.get('userId'))) before reading accounts/creating transactions.

Copilot uses AI. Check for mistakes.
Comment thread server/routes/import.ts
Comment on lines +1081 to +1086
const lines = body.split('\n').filter((l) => l.trim());
if (lines.length < 2) {
return c.json({ error: 'CSV must have header + data rows' }, 400);
}

const cols = parseCSVLine(lines[0]);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

CSV header parsing doesn’t strip a UTF-8 BOM. If the export includes a BOM (common with Excel/Google Drive), colIdx('Payment ID') can fail and the route will incorrectly report missing required columns. Consider normalizing line endings and stripping a leading \uFEFF from the header line before calling parseCSVLine.

Suggested change
const lines = body.split('\n').filter((l) => l.trim());
if (lines.length < 2) {
return c.json({ error: 'CSV must have header + data rows' }, 400);
}
const cols = parseCSVLine(lines[0]);
const normalizedBody = body.replace(/\r\n?/g, '\n');
const lines = normalizedBody.split('\n').filter((l) => l.trim());
if (lines.length < 2) {
return c.json({ error: 'CSV must have header + data rows' }, 400);
}
const headerLine = lines[0].replace(/^\uFEFF/, '');
const cols = parseCSVLine(headerLine);

Copilot uses AI. Check for mistakes.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Code Review -- PR 106: TurboTenant Deposits + Rent Roll Import

Overview

Adds two new import endpoints to server/routes/import.ts:

  • POST /api/import/turbotenant-deposits: ingests TurboTenant payment/deposit history with dedup, tenant/account resolution, and COA classification
  • POST /api/import/turbotenant-rentroll: reads a rent roll snapshot and returns structured unit data (read-only, no transactions created)

The overall structure is clean and consistent with the existing import handlers. A few issues need attention before merging.


Issues to Fix

1. Bypasses tenant middleware

Every other import endpoint reads const tenantId = c.get('tenantId') from the caller auth context, then scopes all storage calls to that tenant. These two new routes instead call storage.getTenants() and self-resolve tenant IDs from hardcoded slug maps.

A caller authenticated as aribia-city-studio can trigger the deposits import and write transactions into nicholas-bianchi or aribia-mgmt, bypassing the tenant isolation that the middleware enforces. The pattern from /api/import/turbotenant and /api/import/hdpro uses the middleware-provided scope and resolves cross-tenant accounts explicitly -- align with that, or if this is intentionally a cross-tenant admin import, gate it with serviceAuth-only middleware and document why.

2. Silent fallback to wrong tenant if aribia-mgmt slug not found

If aribia-mgmt does not exist in the DB (after a re-seed or in staging), the line tenantId = slugToTenantId['aribia-mgmt'] || Object.values(slugToTenantId)[0] silently attributes unresolved transactions to whatever tenant is first in the query result. This row should be skipped++ with a warning log, not silently misattributed.


Issues Worth Fixing

3. propertyId never set on created transactions

Other import handlers resolve a propertyId from address/name patterns and pass it to createTransaction. Without it, these rent transactions are invisible in property-level P&L and rent roll aggregations. An ADDRESS_PROPERTY_MAP already exists in the rent roll handler -- the deposits handler could use the same lookup.

4. iSecDep column is parsed but never used

const iSecDep = colIdx('Security Deposit') is computed but never read from row data. Remove it or use it.

5. propertyName in ADDRESS_PROPERTY_MAP is dead code

The rent roll handler declares { tenantSlug, propertyName } but propertyName is never accessed. Remove it or use it.

6. Metadata escape-fixup is fragile and inconsistent

The deposits handler backslash-unescapes paymentMethod, leaseAddress, and bankAccount in metadata but not note, leaseTitle, or the description/payee fields. If there is a real escaping bug in parseCSVLine, fix it there -- papering over it per-field will silently miss other sequences.

7. classificationConfidence 0.900 is too high for bank-name heuristics

The existing turbotenant importer uses 0.700 for non-suspense keyword-matched transactions. Using 0.900 for the same quality heuristic (bank account name contains "fee") places these transactions near the top of the classification review queue as near-trusted when they have not earned that level. Use 0.750 or match the existing 0.700.

8. BOM handling in rent roll preamble detection is incomplete

If lines[0] has a BOM but does not start with "pulled on", startLine stays 0 and parseCSVLine(lines[0]) sees the BOM in the first column name, causing colIdx('Property') to return -1. Strip the BOM from lines[0] unconditionally before the preamble check.


Looks Good

  • Dedup via tt-deposit:<paymentId> external ID is consistent with other importers
  • accountsCache avoids N+1 DB queries for account lookups
  • resolveBankAccount sorts by key length (longest-first) for most-specific match
  • ledgerLog called on both endpoints -- audit trail covered
  • Fee vs rental income classification (4010/4020) from bank account name is reasonable
  • Both endpoints return structured result objects consistent with the import API
  • Required-column validation returns 400 early

Test Plan Gap

The current test plan is a manual production smoke test (70 rows from Google Drive). For a route that writes financial transactions, consider a fixture CSV with assertions on import counts and the externalId written, plus a row that exercises the MGMT fallback path to verify it does not silently misattribute.


Summary: The multi-tenant boundary bypass (1) and silent misattribution fallback (2) should be resolved before merge. The missing propertyId (3) is a functional correctness issue for property-level reporting. The rest are cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants