Skip to content

feat(web): distribute funds from parent to child organizations#4260

Merged
RSO merged 5 commits into
mainfrom
helpful-bag
Jun 25, 2026
Merged

feat(web): distribute funds from parent to child organizations#4260
RSO merged 5 commits into
mainfrom
helpful-bag

Conversation

@RSO

@RSO RSO commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an owner/billing_manager-only Distribute funds page for parent organizations that moves available balance to direct child organizations.
  • Transfers are recorded as non-expiring parent_to_child_transfer_out / parent_to_child_transfer_in credit transactions on the parent and each child in a single transaction, with rollups and audit-log entries.
  • The whole action is disabled while the parent has any expiring credits, so we never partially move an expiring credit bucket through the lazy-expiry engine (kept v1 simple and money-safe).

Backend

  • New organizations.funds sub-router (organization-funds-router.ts):
    • childBalances query (owner/billing_manager): parent balance, hasExpiringCredits, and per-child { id, name, balanceMicrodollars }. Runs lazy expiry first so the flag/balances are current.
    • distribute mutation (owner/billing_manager + active sub/trial): locks the parent, rejects when expiring credits exist (PRECONDITION_FAILED), validates total ≤ balance and that each target is a direct non-deleted child, then writes ledger rows + rollups on both sides and audit logs.
  • Adds the organization.funds.distribute_to_children audit action (plain text column, no migration needed).

Frontend

  • useOrganizationChildBalances + useDistributeFundsToChildren hooks.
  • New route organizations/[id]/distribute-funds/ gated to ['owner','billing_manager']: table of children (name, current balance, dollar input), live total/remaining footer, sum-exceeds-balance + per-field validation, and a disabled read-only state with a warning banner when expiring credits exist.
  • Entry point added to the existing parent-only, role-gated child-organizations card.
  • Follows the Kilo design contract: surface-ladder card, tabular-nums money columns, single primary CTA, accessible inline errors (aria-invalid/aria-describedby), skeleton loading.

Testing

  • apps/web typecheck, oxlint, and oxfmt pass.
  • New organization-funds-router.test.ts (6 tests): balances read, distribution math + ledger rows + audit log, over-balance rejection, non-child rejection, and the expiring-credits guard.

Notes

  • Transferred credits use is_free: false (real balance movement, not a new free grant).
  • No new PII, so no GDPR soft-delete changes required.

Adds an owner/billing_manager-only page that lets a parent organization
move available balance to its direct child organizations. Transfers are
recorded as non-expiring credit transactions on both the parent and each
child within a single transaction, with audit-log entries.

The action is disabled entirely while the parent has any expiring credits,
avoiding the need to partially move expiring credit buckets through the
lazy-expiry engine.
Comment thread apps/web/src/routers/organizations/organization-funds-router.ts
Comment thread apps/web/src/app/(app)/organizations/[id]/distribute-funds/page.tsx
@kilo-code-bot

kilo-code-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/DistributeFundsPage.tsx
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/parseDollarInput.test.ts
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/parseDollarInput.ts
Previous Review Summaries (3 snapshots, latest commit 8d030da)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 8d030da)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/DistributeFundsPage.tsx
  • apps/web/src/routers/organizations/organization-funds-router.test.ts
  • apps/web/src/routers/organizations/organization-funds-router.ts

Previous review (commit 5a64d27)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • apps/web/src/routers/organizations/organization-funds-router.test.ts
  • apps/web/src/routers/organizations/organization-funds-router.ts

Previous review (commit f74c897)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/routers/organizations/organization-funds-router.ts 95 Child balances can be stale after credits expire
apps/web/src/app/(app)/organizations/[id]/distribute-funds/page.tsx 12 Expired-trial organizations can still reach an interactive transfer form
Files Reviewed (8 files)
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/DistributeFundsPage.tsx - 0 issues
  • apps/web/src/app/(app)/organizations/[id]/distribute-funds/page.tsx - 1 issue
  • apps/web/src/app/api/organizations/hooks.ts - 0 issues
  • apps/web/src/components/organizations/OrganizationChildOrganizationsCard.tsx - 0 issues
  • apps/web/src/routers/organizations/organization-funds-router.test.ts - 0 issues
  • apps/web/src/routers/organizations/organization-funds-router.ts - 1 issue
  • apps/web/src/routers/organizations/organization-router.ts - 0 issues
  • packages/db/src/schema-types.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by gpt-5.4-20260305 · Input: 47.6K · Output: 5.7K · Cached: 217.3K

Review guidance: REVIEW.md from base branch main

RSO added 3 commits June 25, 2026 15:03
A transfer is a balance movement, not a purchase. Recording the ledger
rows as is_free: false fabricated paid provenance via hasOrganizationEverPaid
(which gates deployments and notifications) for children funded by free
credits, and for parents funded only by free credits. Mark both the
incoming and outgoing transfer rows as is_free: true.
childBalances previously read children's cached acquired/used values
directly, so a child with a past-due expiry could show an overstated
current balance. Process due expirations per child before returning,
mirroring the parent path.
The page only checked role membership, but the transfer mutation also
requires an active subscription/trial. On an expired-trial (read-only)
org, owners/billing managers could open the form and attempt a transfer,
hitting FORBIDDEN at submit. Wrap the form in LockableContainer so it
renders non-interactive with the standard lock UI, matching other
organization settings pages.
Move the dollar-amount parsing logic out of DistributeFundsPage into a
co-located, pure parseDollarInput helper and cover it with unit tests
(whole/fractional amounts, commas, decimal-place limit, non-numeric and
negative input, and non-finite guard). Addresses PR review feedback.
@RSO RSO merged commit 9db454a into main Jun 25, 2026
62 checks passed
@RSO RSO deleted the helpful-bag branch June 25, 2026 15:21
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