Skip to content

OUT-3840: bill company invoices to earliest active client when company name is off - #57

Merged
SandipBajracharya merged 4 commits into
mainfrom
OUT-3840
Jun 9, 2026
Merged

OUT-3840: bill company invoices to earliest active client when company name is off#57
SandipBajracharya merged 4 commits into
mainfrom
OUT-3840

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

Fixes OUT-3840. When Use company name is off and an invoice is billed to a company (no clientId), the contact sync previously threw Cannot use company name of placeholder company for invoice for every such invoice — even non-placeholder companies. It now falls back to billing the earliest active client of the company.

Changes

  • Add getEarliestActiveClient (src/lib/copilot/utils.ts) — filters clients to status === 'active' and picks the earliest by createdAt.
  • Rework the billing-target resolution in SyncedContactsService#getSyncedContact:
    • off + company → earliest active client (throws No active client found for company if none)
    • on + company → company name (still rejects placeholder companies)
    • client present (either flag) → bill that client directly
  • Clarity cleanup: simplify the branch conditions, fix misleading comments, rename tempClientIdbilledClientId.

Billing matrix

Use company name Billed to Result
on company company contact
on client client contact
off company earliest active client
off client client contact

Notes

  • Client status values are notInvited / invited / active; only active is selected.
  • Behavior change vs main: flag-on invoices billed to an individual client now sync as the client contact (previously the company). Existing customers synced as company contacts under the old logic may get a new client contact created in Xero — no backfill.

Verification

  • pnpm typecheck
  • pnpm lint

🤖 Generated with Claude Code

…ompany name is off

When useCompanyName is off and an invoice is billed to a company, the
sync previously threw "Cannot use company name of placeholder company".
Fall back to the earliest active client of the company instead, and add
getEarliestActiveClient to select it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 8, 2026

Copy link
Copy Markdown

OUT-3840

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xero-integration Ready Ready Preview, Comment Jun 9, 2026 5:34am

Request Review

@supabase

supabase Bot commented Jun 8, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project pkdwtcdqcefmlgxmcwmc because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@SandipBajracharya SandipBajracharya changed the title fix(OUT-3840): bill company invoices to earliest active client when company name is off OUT-3840: bill company invoices to earliest active client when company name is off Jun 8, 2026
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a regression where useCompanyName=off invoices billed to a company (no clientId) incorrectly threw the "placeholder company" error for every company. The fix adds a new getEarliestActiveClient helper and restructures getSyncedContact into three explicit branches based on the flag and whether a clientId is present.

  • New helper (src/lib/copilot/utils.ts): filters clients by status === 'active' and picks the earliest by createdAt using Date object comparison, correctly handling timezone-offset strings.
  • getSyncedContact rework: introduces billedClientId to track the resolved contact ID; DB queries and the stale-row delete all consistently derive their target from useNonPlaceholderCompanyName || !billedClientId; the useCompanyName=on + clientId path now bills to the client rather than the company, as noted.
  • getCompanyClients uses limit: 10000 (pre-existing pattern), which is sufficient for typical portal sizes without requiring pagination.

Confidence Score: 5/5

Safe to merge — the three billing branches are logically sound, DB query conditions derive consistently from the resolved state, and the getEarliestActiveClient helper handles all edge cases including empty lists and timezone-aware timestamps.

The billing-target resolution was cleanly restructured into three explicit, non-overlapping branches. The billedClientId rename carries the resolved identity correctly through every DB read, delete, and log call. The getCompanyClients call uses the pre-existing limit: 10000 pattern. No data path produces wrong state or silently drops errors.

No files require special attention.

Important Files Changed

Filename Overview
src/lib/copilot/utils.ts Adds getEarliestActiveClient — filters clients to active status then picks the earliest by createdAt using proper Date object comparison; logic is correct and handles the empty-array edge case.
src/features/invoice-sync/lib/SyncedContacts.service.ts Reworks billing-target resolution in getSyncedContact — three clear branches (no company name + no clientId, company name on + no clientId, clientId present); DB query conditions and delete path follow correctly from useNonPlaceholderCompanyName/billedClientId state.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getSyncedContact\nclientId?, companyId] --> B{clientId\npresent?}
    B -- No --> C{useCompanyName?}
    C -- off --> D[getCompanyClients companyId\ngetEarliestActiveClient]
    D --> E{active client\nfound?}
    E -- No --> F[throw: No active client\nfor company 400]
    E -- Yes --> G[billedClientId = client.id\nuserType = CLIENT]
    C -- on --> H[getCompany companyId]
    H --> I{isPlaceholder?}
    I -- Yes --> J[throw: Cannot use company name\nof placeholder 400]
    I -- No --> K[useNonPlaceholderCompanyName = true\nuserType = COMPANY]
    B -- Yes --> L[billedClientId = clientId\nuserType = CLIENT]
    G --> M[Query syncedContacts\nby billedClientId / CLIENT]
    K --> N[Query syncedContacts\nby companyId / COMPANY]
    L --> O[Query syncedContacts\nby clientId / CLIENT]
    M & N & O --> P{Row found &\nXero contact valid?}
    P -- Yes --> Q[validateXeroContact\nreturn contact]
    P -- No --> R[Delete stale row\ncreateContact]
Loading

Reviews (2): Last reviewed commit: "refactor(OUT-3840): address PR review fe..." | Re-trigger Greptile

Comment thread src/lib/copilot/utils.ts Outdated
Comment thread src/features/invoice-sync/lib/SyncedContacts.service.ts Outdated
Comment thread src/features/invoice-sync/lib/SyncedContacts.service.ts
- Compare client createdAt as Date objects instead of lexically
- Drop redundant data object that duplicated the APIError message

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread src/lib/copilot/utils.ts Outdated
@SandipBajracharya
SandipBajracharya merged commit 97bd8ce into main Jun 9, 2026
5 checks passed
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