Skip to content

feat(finance): vendor spend control — cc_vendors + deterministic spend-risk#119

Open
chitcommit wants to merge 2 commits into
mainfrom
claude/vibrant-pasteur-jc090f
Open

feat(finance): vendor spend control — cc_vendors + deterministic spend-risk#119
chitcommit wants to merge 2 commits into
mainfrom
claude/vibrant-pasteur-jc090f

Conversation

@chitcommit

Copy link
Copy Markdown
Contributor

Why

"Get finance under control." The GitHub Actions billing block that kicked this off was exactly the failure mode ChittyCommand exists to catch: a recurring vendor bill whose autopay / spending-limit risk wasn't surfaced before it bounced. ChittyCommand already models money as obligations with urgency scoring — but had no concept of vendor spend, budgets, or autopay-bounce risk. This adds that surface.

Scope note: this session is scoped to chittyos/chittycommand. The cross-repo pieces from the broader finance workstream — Cloudflare spending-limit changes, the GitHub org billing unblock, chittyagent-finance billing-API ingestion, the Notion weekly summary — live in other repos and operator lanes and are intentionally not in this PR. I did not write untested billing-API client stubs for vendors I can't reach from here; ingestion is left pluggable.

What

  • migrations/0019_vendors.sql — new cc_vendors table: billing cycle, expected/MTD spend, next-bill date, auto_pay + payment_status (active|failed|limited|unknown), provider spending_limit, internal budget_limit, status (active|paused|cancelled|zombie), risk_score. Additive + idempotent; self-defines the shared cc_update_timestamp() trigger fn so it applies cleanly on the journaled-only test DB. Registered in tests/setup/global-setup.ts (ADDITIVE_PREFIXES).
  • src/lib/vendor-risk.ts — pure, deterministic computeVendorRisk() (sibling of urgency.ts): failed-autopay (+50), spend-limit headroom, budget overrun, bill imminence (manual > autopay), with a healthy-autopay credit. Returns { score, level, reasons[] }, reusing urgencyLevel so vendors and obligations share one risk vocabulary.
  • src/routes/vendors.ts/api/vendors: GET / (filters incl. at_risk), GET /summary (MTD by category, at-risk vendors, next-30-day bills, zombies), GET /:id, POST / (upsert by vendor_name), PATCH /:id, POST /recompute-risk.
  • MCP: query_vendors + get_vendor_risk (portfolio risk lens for Claude sessions). Tool counts 50→52 unscoped / 54→56 scoped; tests/mcp.test.ts updated.
  • Cron: weekly vendor-risk sweep over existing rows (no external feed) in the Monday utility_scrape cadence.
  • Docs: schema.ts, validators.ts, CLAUDE.md updated.

Testing

  • npm run typecheck — clean.
  • npx vitest run tests/lib/vendor-risk.spec.ts tests/mcp.test.ts52 passed (full boundary coverage of the risk engine + the updated MCP tool-count assertions). Integration specs that need a live DATABASE_URL were not exercised in this sandbox.

Follow-ups (out of scope here)

  • Billing-API ingestion adapters (GitHub/CF/Anthropic/OpenAI/Neon/1Password) → chittyagent-finance lane.
  • Surface vendor at-risk count on the main dashboard payload (left untouched to avoid changing an existing UI contract).
  • Operator actions: unblock GH org Actions billing, raise CF spending limit.

https://claude.ai/code/session_015mkdG1VYH3AdqLe4E3i9H6


Generated by Claude Code

…d-risk

Stand up a vendor spend-control surface so recurring org/operational vendors
(GitHub, Cloudflare, Anthropic, OpenAI, Neon, 1Password, …) are tracked with
autopay-bounce / budget / spend-limit risk — the failure mode behind a surprise
billing block.

- migration 0019: cc_vendors (billing cycle, expected/MTD spend, next bill,
  autopay + payment_status, provider spending_limit, internal budget, status)
- src/lib/vendor-risk.ts: pure deterministic computeVendorRisk() (sibling of
  urgency.ts), unit-tested in tests/lib/vendor-risk.spec.ts
- /api/vendors: list, /summary rollup (MTD by category, at-risk, upcoming
  bills, zombies), get / create(upsert) / patch / recompute-risk
- MCP: query_vendors + get_vendor_risk (tool counts 50→52 / 54→56)
- cron: weekly vendor-risk sweep over existing rows (no external feed)
- global-setup applies 0019; schema.ts, validators.ts, CLAUDE.md updated

Ingestion stays pluggable (POST/PATCH today); live billing-API ingestion is the
cross-repo chittyagent-finance follow-up.

https://claude.ai/code/session_015mkdG1VYH3AdqLe4E3i9H6
@github-actions

Copy link
Copy Markdown
  1. @coderabbitai review
  2. @copilot review
  3. @codex review
  4. @claude review
    Adversarial review request: evaluate security, policy bypass paths, regression risk, and merge-gating bypass attempts.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@chitcommit, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 36 minutes and 27 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 206f39ae-07b7-4ede-836f-31fa19c78398

📥 Commits

Reviewing files that changed from the base of the PR and between 6a26e66 and 35fe012.

📒 Files selected for processing (12)
  • CLAUDE.md
  • migrations/0019_vendors.sql
  • src/db/schema.ts
  • src/index.ts
  • src/lib/cron.ts
  • src/lib/validators.ts
  • src/lib/vendor-risk.ts
  • src/routes/mcp.ts
  • src/routes/vendors.ts
  • tests/lib/vendor-risk.spec.ts
  • tests/mcp.test.ts
  • tests/setup/global-setup.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vibrant-pasteur-jc090f

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.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 11, 2026

Copy link
Copy Markdown

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 successful!
View logs
chittycommand 35fe012 Jun 11 2026, 11:43 PM

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

Copy link
Copy Markdown

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: aadd0c7d63

ℹ️ 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 src/routes/mcp.ts Outdated
WHERE (${category}::text IS NULL OR category = ${category})
AND (${status}::text IS NULL OR status = ${status})
ORDER BY risk_score DESC NULLS LAST, next_bill_date ASC NULLS LAST
LIMIT ${limit}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply at-risk filtering before limiting vendors

When query_vendors is called with at_risk: true, this LIMIT is applied before the live risk recomputation and filtering at lines 1161-1165. In a portfolio with more than the default 20 vendors, any high-risk vendor whose stored risk_score is stale/null or simply sorts after the first page is omitted, so the tool can return an empty or incomplete at-risk list even though matching vendors exist.

Useful? React with 👍 / 👎.

Comment thread src/routes/vendors.ts
status = COALESCE(${body.status ?? null}, status),
owner = COALESCE(${body.owner ?? null}, owner),
account_id = COALESCE(${body.account_id ?? null}, account_id),
risk_score = ${riskScore},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist metadata updates in vendor PATCH

When clients include metadata in a PATCH request, updateVendorSchema accepts the field but this UPDATE statement never assigns it, so the request succeeds while silently leaving the stored metadata unchanged. This affects any partial vendor edit that is meant to update ownership/context notes or integration metadata.

Useful? React with 👍 / 👎.

// Post-consolidation, additive hand-rolled migrations that complement the
// journaled drizzle schema (not part of the alternative-history set).
const ADDITIVE_PREFIXES = ['0017_', '0018_'];
const ADDITIVE_PREFIXES = ['0017_', '0018_', '0019_'];

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 Include the vendors migration in the deploy migration path

Adding 0019_ here only makes Vitest apply the new SQL; the production npm run db:migrate path is drizzle-kit migrate, which uses migrations/meta/_journal.json, and that journal has no 0019_vendors entry. In any environment migrated with the documented command, the newly mounted /api/vendors routes and MCP tools will query cc_vendors before the table exists, causing runtime failures until the migration is added to the real migration path.

Useful? React with 👍 / 👎.

…limit

- vendors PATCH now persists `metadata`: updateVendorSchema accepted the field
  but the UPDATE never assigned it (silent no-op on ownership/context edits)
- query_vendors MCP tool recomputes risk live and applies the at_risk filter
  BEFORE limiting, so high-risk vendors that sort past a page edge or carry a
  stale/null stored risk_score are no longer dropped
- document migration 0019's prod apply path (psql) in its header — the drizzle
  journal ends at 0005, so 0006–0019 are hand-rolled and applied via psql, not
  `npm run db:migrate`

https://claude.ai/code/session_015mkdG1VYH3AdqLe4E3i9H6
@github-actions

Copy link
Copy Markdown
  1. @coderabbitai review
  2. @copilot review
  3. @codex review
  4. @claude review
    Adversarial review request: evaluate security, policy bypass paths, regression risk, and merge-gating bypass attempts.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Copy link
Copy Markdown
Contributor Author

Thanks for the review — addressed in 35fe012:

  • P2 — vendors PATCH metadata (src/routes/vendors.ts): fixed. updateVendorSchema accepted metadata but the UPDATE never assigned it. The statement now sets metadata = COALESCE(${...}::jsonb, metadata) so partial edits persist ownership/context notes.
  • P2 — query_vendors at_risk before LIMIT (src/routes/mcp.ts): fixed. The MCP tool now fetches matching rows, recomputes risk live (stored risk_score can be stale/null), and applies the at_risk filter + sort before slicing to limit, so a high-risk vendor past a page edge is no longer dropped. (The HTTP GET /api/vendors route already filtered before limiting; only the MCP tool had the early LIMIT.)
  • P1 — migration in the deploy path: working as intended, not an oversight. The drizzle journal (migrations/meta/_journal.json) ends at 0005_sour_dreadnoughts; migrations 0006–0019 are all hand-rolled and applied via psql "$DATABASE_URL" < migrations/NNNN.sql (see docs/plans/*), not drizzle-kit migrate. 0019 follows the same path as 0017/0018. Running drizzle-kit generate here would diff schema.ts against the stale 0005 snapshot and emit a spurious migration, so that's deliberately avoided. The migration is idempotent (CREATE TABLE IF NOT EXISTS / CREATE OR REPLACE FUNCTION / DROP TRIGGER IF EXISTS), and I added the explicit psql apply step to the 0019_vendors.sql header. Same deploy step the table's siblings (cc_tasks 0011, cc_scrape_jobs 0013, etc.) already require.

Generated by Claude Code

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