feat(web): distribute funds from parent to child organizations#4260
Merged
Conversation
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.
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
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)
Previous review (commit 5a64d27)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit f74c897)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (8 files)
Reviewed by gpt-5.4-20260305 · Input: 47.6K · Output: 5.7K · Cached: 217.3K Review guidance: REVIEW.md from base branch |
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.
marius-kilocode
approved these changes
Jun 25, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
parent_to_child_transfer_out/parent_to_child_transfer_incredit transactions on the parent and each child in a single transaction, with rollups and audit-log entries.Backend
organizations.fundssub-router (organization-funds-router.ts):childBalancesquery (owner/billing_manager): parent balance,hasExpiringCredits, and per-child{ id, name, balanceMicrodollars }. Runs lazy expiry first so the flag/balances are current.distributemutation (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.organization.funds.distribute_to_childrenaudit action (plaintextcolumn, no migration needed).Frontend
useOrganizationChildBalances+useDistributeFundsToChildrenhooks.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.tabular-numsmoney columns, singleprimaryCTA, accessible inline errors (aria-invalid/aria-describedby), skeleton loading.Testing
apps/webtypecheck, oxlint, and oxfmt pass.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
is_free: false(real balance movement, not a new free grant).